Java教程

Java线程深入学习(二)

本文主要是介绍Java线程深入学习(二),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

第三章、线程同步

3.1 线程同步机制

3.2 锁

3.2.1 锁的作用

3.2.2 锁相关的概念

3.3 内部锁:synchronized关键字

3.3.1 synchronized同步代码块

3.4 轻量级同步机制:volative关键字

3.4.1 volatile的作用

3.4.2 volatile非原子特性

3.4.3 常用的原子类进行自增自减操作

3.5 CAS

3.6 原子变量类

3.6.1 AtomicLong

3.6.2 AtomicIntegerArray

3.6.3 AtomicIntegerFieldUpdater

3.6.4 AtomicReference


第三章、线程同步

3.1 线程同步机制

   线程同步机制是一套用于协调线程之间的数据访问的机制,该机制可以保障线程安全。Java平台提供的线程同步机制包括:锁、volatile关键字、final关键字、static关键字以及相关的API,如Object.wait()、Object.notify()等。

3.2 锁

   线程安全问题的产生前提是多个线程并发访问共享数据。将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问。锁就是复用这种思路来保障线程安全的。

   锁Lock可以理解为对共享数据进行保护的一个许可证。对于同一个锁保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该锁,一个线程只有在持有锁的情况下才能对这些共享数据进行访问。并且一个锁一次只能被一个线程单独持有。拥有锁的线程在结束对共享数据的访问后必须释放其持有的锁。

   一个线程在访问共享数据前必须先获得锁,获得锁的线程称为锁的持有线程;一个锁一次只能被一个线程持有。锁的持有线程在获得锁之后和释放锁之前所执行的代码称为临界区(Critical Section)。

   锁具有排他性(Exclusive),即一个锁一次只能被一个线程持有,这种锁称为排他锁或互斥锁Mutex。

   JVM把锁分为内部锁显示锁两种:内部锁通过synchronized关键字实现;显示锁通过java.concurrent.locks.Lock接口的是实现类实现。

3.2.1 锁的作用

   锁可以实现对共享数据的安全访问,保障线程的原子性、可见性与有序性。

   锁是通过互斥保障原子性,一个锁只能被一个线程持有,这就保证临界区的代码一次只能被一个线程执行,使得临界区代码所执行的操作自然而然地具有不可分割的特性,即具备了原子性。

   可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器的缓存这两个动作实现的。在Java平台中,锁的获得隐含着刷新处理器缓存的动作,锁的释放隐含着冲刷处理器缓存的动作。

   锁能够保障有序性,写线程在临界区所执行的动作在读线程所执行的临界区看来像是完全按照源码顺序执行的。

   注意:使用锁保障线程的安全性,必须满足以下条件:

        a.这些线程在访问共享数据时必须使用同一个锁;

        b.即使是读共享数据的线程也需要使用同步锁。

3.2.2 锁相关的概念

   ①可重入性Reentrancy:描述了这样一个问题,一个线程持有该锁的时候能否再次(多次)申请该锁。如果一个线程持有一个锁的时候还饿能够继续成功申请该锁,称该锁是可重入的,否则就称该锁为不可重入的。

        比如:

void methodA{
    此处申请锁A
    调用methodB();
    此处释放锁A
}

void methodB{
    此处申请锁A
    执行动作
    此处释放锁A
}

        如果一个线程在获得锁A后调用方法B,在方法B中仍能够获得锁A执行动作,说明该锁就是可

      重入的。如果申请不成功,就是不可重入的。

   ②锁的争用与调度。Java平台中内部锁属于非公平锁;显示Lock锁既支持公平锁又支持非公平锁。

   ③锁的粒度。粒度指一个锁可以保护的共享数据的数量大小。锁保护的共享数据量大,称该锁的粒度粗,否则就称该锁的粒度细。锁的粒度过粗会导致线程在申请锁时会进行不必要的等待;锁的粒度过细会增加锁调度的开销。

3.3 内部锁:synchronized关键字

   Java中的每个对象都有一个与之关联的内部锁(Intrinsic lock)。这种锁也被称为监视器(Monitor),这种内部锁是一种排他锁,可以保障原子性、可见性与有序性。

   内部锁是通过synchronized关键字实现的,synchronized关键字修饰代码块,修饰该方法,语法格式为:

        synchronized(对象锁){

                同步代码块,可以在同步代码块中访问共享数据

        }

   修饰实例方法称为同步实例方法;修饰静态方法称为同步静态方法。

3.3.1 synchronized同步代码块

public class Test01{
    public static void main(String[] args){
        //创建两个线程分别打印数字
        Test01 obj = new Test01();
        new Thread(new Runnable(){
            public void run(){
                obj.mm();//锁对象此时就是obj对象
            }
        }).start();
        //调用同一个对象的mm()方法
        new Thread(new Runnable(){
            public void run(){
                obj.mm();//此时obj对象被第一个线程占用,等其调用mm()方法结束释放锁,第二个线程才可以得到所开始执行mm()方法,之前一直位于等待区进行等待
            }
        }).start();

        //必须两个线程需要的锁都是obj才会发生同步机制
    }
    
    //打印数字
    publci void mm(){
        synchronized(this){//以当前对象作为锁
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
}

   这个例子中如果不加锁的话,不能各自执行循环,循环结果会乱套。

   如果线程需要的锁不同,不会实现同步机制。

   可以使用常量作为锁对象,因为常量也是这个类创建出来的对象共享的。

public class Test01{
    public static void main(String[] args){
        //创建两个线程分别打印数字
        Test01 obj = new Test01();
        new Thread(new Runnable(){
            public void run(){
                obj.mm();
            }
        }).start();

        Test01 obj2 = new Test01();
        new Thread(new Runnable(){
            public void run(){
                obj2.mm();
            }
        }).start();

        //虽然是两个对象obj和obj2,但是这两个对象共享了一个OBJ常量
        
        //使用同一个锁对象,但是方法不同也会实现同步
        new Thread(new Runnable(){
            public void run(){
                Test01.mm2();
            }
        }).start();
    }
    
    public static final Object OBJ = new Object();
    
    public void mm(){
        synchronized(OBJ){//以常量作为锁
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }

    public static void mm1(){
        synchronized(OBJ){//以常量作为锁
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
}

   使用synchronized修饰实例方法,默认this作为锁对象

public class Test01{
    public static void main(String[] args){
        //创建两个线程分别打印数字
        Test01 obj = new Test01();
        new Thread(new Runnable(){
            public void run(){
                obj.mm();
            }
        }).start();

        new Thread(new Runnable(){
            public void run(){
                obj.mm();
            }
        }).start();
    }
    
    public synchronized void mm(){//修饰实例方法,默认this为锁对象
        for(int i = 0;i < 100;i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }

    public synchronized void mm2(){
        synchronized(this){//this为锁对象
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
}

   synchronized修饰静态方法,默认的锁对象是当前类的运行时类对象,也被称为类锁

public class Test01{
    public static void main(String[] args){
        //创建两个线程分别打印数字
        Test01 obj = new Test01();
        new Thread(new Runnable(){
            public void run(){
                mm();
            }
        }).start();
        
        new Thread(new Runnable(){
            public void run(){
                obj.mm2();
            }
        }).start();
    }
    
    public synchronized static void mm(){//修饰静态方法,默认运行时类为锁对象
        for(int i = 0;i < 100;i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }

    public synchronized void mm2(){
        synchronized(Test01.class){//当前类对象为锁对象
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
}

   同步方法锁的粒度粗,执行效率低;同步代码块锁的粒度高,执行效率高。一般均选择使用同步代码块锁

   同步过程中如果线程出现异常,会自动释放锁对象

   死锁:在多线程程序中,同步时可能需要使用多个锁,如果获得锁的顺序不一致,可能会导致死锁。比如线程A要先获得锁1,再获得锁2;而线程B要先获得锁2,再获得锁1。如果此时线程A先获得了锁1,线程B先获得了锁2,此时线程A要等待线程B的锁2,线程B要等待线程A的锁1,那个锁都不能释放,哪个锁都不能获得,就陷入了死锁。

   如何避免死锁?当需要多个锁时,所有线程获得锁的顺序保持一致即可。

3.4 轻量级同步机制:volative关键字

3.4.1 volatile的作用

   关键的作用是使变量在多个线程之间可见。可以强制线程从公供内存中读取变量的值,而不是从工作内存中读取,从而实现多个内存之间可见。

public class Test02{
    public static void main(String[] args){
        
        PrintString printString = new PrintString();
        //创建一个子线程   
        new Thread(new Runnable(){
            public void run(){
                printString.printStringMethod();
            }
        })
        
        //中间可以加一个睡眠,让效果更明显
        printString.setContinuePrint(false);//如果不用volatile修饰,子线程可能读不到标志位置为false
    }
}

class PrintString{
    private volatile boolean continuePrint = true;//volatile强制线程从公共内存中读取变量的值

    public PringString setContinuePrint(boolean continuePrint){

        this.continuePrint = continuePrint;
        return this;
    } 

    public void printStringMethod(){
        while(continuePrint){
        }
    }
}

   volatile与synchronized比较

        ①volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好;随着

          JDK新版本的发布,synchronized的执行效率也有较大的提升,在开发中使用synchronized

          的比率还是很大的。

        ②volatile只能修饰变量,而synchronized可以修饰方法、代码块。

        ③多线程访问volatile变量不会发生阻塞,而synchronized可能阻塞。

        ④volatile能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也

          可以保证可见性。

        ⑤volatile解决的是变量在多个线程之间的可见性;synchronized关键字解决多个线程之间访

          问公共资源的同步性。

3.4.2 volatile非原子特性

   volatile关键字增加了实例变量在多个线程之间的可见性,但是不具备原子性。

3.4.3 常用的原子类进行自增自减操作

   我们知道i++不是原子操作,除了使用synchronized进行同步之外,也可以使用AtomicInteger/AtomicLong原子类实现。

public class Test{
    public static void main(String[] args){
        for(int i = 0;i < 10;i++){
            new MyThread().start();
        }
        
        System.out.println(MyThread.count.get());//此处读取的数一定是1000*10=10000,因为此时的自增有了原子性
    } 
}

class MyThread extends Thread{
    //使用AtomicInteger对象
    private static AtomicInteger count = new atomicInteger();

    public static void addCount(){
        for(int i = 0;i < 1000;i++){
            count.getAndIncrement();//表示自增后缀形式
        }
    } 

    public void run(){
        addCounr();
    }
}

3.5 CAS

   CAS(Compare And Swap)是由硬件实现的。CAS可以将read-modify-write这类操作转换为原子操作。比如i++自增操作包括三个子操作:从主内存读取i变量值;对i的值加1;再把加1之后的值保存到主内存。

   CAS原理:在把数据更新到主内存前,再次读取主内存变量的值,如果现在变量的值与期望的值(操作起始时读取的值)一样就更新。不一样的话就撤销(或者重新进行)本次操作。 

   使用CAS这种思想实现一个线程安全的计数器:

public class CASTest{
    public static void main(String[] args){
        CASCounter casCounter = new CASCounter();

        for(int i = 0;i < 1000;i++){
            new Thread(new Runnable(){
                public void run(){
                    System.out.println(casCounter.incrementAndGet());
                }
            }).start();
        }
    }
}

class CASCounter{
    //volatile修饰value,使value值对所有线程可见
    volatile private long value;

    public long getValue(){
        return value;
    }

    //定义campare and swap
    private boolean compareAndSwap(long expectedValue,long newValue){
        //如果当前value的值与期望的expectedValue值一样,就把当前的value字段替换为newValue
        //替换过程应该同步
        synchronized(this){
            if(value == expectedValue){//如果当前读到的value值和期望的值(自增1操作前读取到的value值)一样
                value = newValue;//赋予新值
                return true;
            }else{
                return false;
            }
        }
    }

    //定义自增的方法
    public long incrementAndGet(){
        long oldValue;
        long newValue;
        do{
            oldValue = value;//当前读到的值
            newValue = oldValue + 1;//对当前读到的值进行加1操作
        }while(!(compareAndSwap(oldValue,newValue)));//如果不返回true就一直循环
        return newValue;
    }
}

   CAS实现原子性的背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过。实际上这种假设不一定称立。

        比如此时有一个共享变量count=0。有一个当前线程第一次读取该值为0,之后想对该值进行修改,期间有一个线程A对count值修改为10,线程B又将count值修改为0,修改后当前线程再次看到count值为0,现在是否认为count变量的值没有被其他线程更新过呢,这种结果是否能够接受?

   这就是CAS中的ABA问题,即共享变量经历了A->B->A的更新。是否能够接受ABA问题与实现的算法有关。如果想要规避ABA问题,可以为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会增加,此时期望值和修订号组成了一个元组来判断是否修改前的值和修改后的值是否一致,ABA变量更新过程:[A,0]->[B,1]->[A,2],每次对共享变量的修改都会导致修订号的增加,通过修订号依然可以准确判断变量是否被其他线程修改过。AtomicStampedReference类就是基于这种思想产生的。

3.6 原子变量类

   原子变量类基于CAS实现,当对共享变量进行read-modify-write更新操作时,通过原子变量类可以保障操作的原子性与可见性。对变量的read-modify-write更新操作是指当前操作不是一个简单的赋值,而是变量的新值依赖变量的旧值,如自增操作i++。由于volatile只能保证可见性,无法保障原子性,原子变量内部类就是借助一个volatile变量,并且保障了改变量的read-modify-write操作的原子性,有时把原子变量类看作增强的volatile变量。原子变量类有12个。

分组原子变量类
基础数据型AtomicInteger   AtomicLong   AtomicBoolean
数组型AtomicIntegerArray   AtomicLongArray   AtomicReferenceArrray
字段更新器AtomicIntegerFieldUpdater   AtomicLongFieldUpdater   AtomicReferenceFieldUpdater   
引用型AtomicReference   AtomicStampedReference   AtomicMarkableReference

3.6.1 AtomicLong

   使用AtomicLong定义一个计数器。

//模拟统计服务器的请求总数、处理成功数、处理失败数
public class Test{
    public static void main(String[] args){
        //通过线程模拟请求,在实际应用中,可以在过滤器中调用计数器的相关方法
        for(int i = 0;i < 1000;i++){
            new Thread(new Runnable(){
                public void run(){
                    Indicator.getInstance().newRequestReceive();
                    int num = new Random.nextInt();
                    if(num % 2 == 0){
                        Indicator.getInstance().requestProcessSuccess();
                    }else{
                        Indicator.getInstance().requestProcessFailure();
                    }
                }
            })
        }
    }
}

//使用原子变量类定义一个计数器
class Indicator{
    //构造方法私有化
    private Indicator(){
    }
    //定义一个私有的本类静态的对象,使得不管在哪用都是使用的同一个计数器
    private static final Indicator INSTANCE = new Indicator();
    //提供一个公共静态方法返回该类唯一实例
    public static Indicator getInstance(){
        return INSTANCE;
    }
    
    //使用原子变量类保存请求总数、成功总数、失败总数
    private final AtomicLong requestCount = new AtomicLong(0);
    private final AtomicLong successCount = new AtomicLong(0);
    private final AtomicLong failureCount = new AtomicLong(0);

    //有新的请求
    public void newRequestReceive(){
        requestCount.incrementAndGet();
    }
    //处理成功
    public void requestProcessSuccess(){
        successCount.incrementAndGet();
    }
    //处理失败
    public void requestProcessFailure(){
        failureCount.incrementAndGet();
    }
    
    //查看总数、成功数、失败数
    public long getRequestCount(){
        return requestCount;
    }
    public long getSuccessCount(){
        return SuccessCount;
    }
    public long getFailureCount(){
        return failureCount;
    }
}

3.6.2 AtomicIntegerArray

   原子更新数组。

public class Test{
    public static void main(String[] args){
        //创建一个指定长度的原子数组
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray();
        
        //返回指定位置的元素
        System.out.println(atomicIntegerArray.get(2));
        
        //设置指定位置的元素
        atomicIntegerArray.set(0,10);
        
        //在设置数组指定下标的元素时,同时返回数组元素的旧值
        Systemout.println(atomicIntegerArray.getAndSet(1,11));
        
        //修改数组元素的值,把数组元素加上某个值
        System.out,println(atomicIntegerArray.addAndGet(0,22));
        System.out,println(atomicIntegerArray.getAndAdd(1,33));
        
        //CAS操作
        //如果数组中索引值为0的元素的值是32的话,就修改为222
        System.out,println(atomicIntegeyArray.compareAndSet(0,32,222));

        //自增/自减
        System.out.println(atomicIntegerArray.incrementAndGet(0));//++i
        System.out.println(atomicIntegerArray.getAndIncrement(0));//i++
        System.out.println(atomicIntegerArray.decrementAndGet(0));//--i
        System.out.println(atomicIntegerArray.getAndDecrement(0));//i--
    }
}

3.6.3 AtomicIntegerFieldUpdater

   可以对原子整数字段进行更新,要求:

        ①字符必须使用volatile修饰,使线程之间可见;

        ②只能是实例变量,不能是静态变量,也不能用final修饰。

   是一个抽象类。

public class Test{
    public static void main(String[] args){
        User user - new User(1234,10);
        
        //开启十个线程
        for(int i = 0;i < 10;i++){
            new SubThread(user).start();
        }
        
        Thread.sleep(1000);//异常自己处理
        
        System.out.println(user);
    }
}


class User{
    int id;
    volatile int age;
    
    public User(int id,int age){
        this.id = id;
        this.age = age;
    }

    //重写toString方法
}


class SubThread extends Thread{
    private User user;
    //创建AtomicIntegerFieldUpdater更新器
    private AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
    
    public SubThread(User user){
        this.user = user;
    }

    public void run(){
        //在子线程中对user对象的age字段自增10次
        for(int i = 0;i < 10;i++){
            System.out.println(updater.getAndIncrement(user));
        }
    }
}

3.6.4 AtomicReference

   可以原子读写一个对象。

public class Test{
    private static AtomicReference<String> atomicReference = new AtomicReference<>("abc");

    public static void main(String[] args){
        //创建100个线程修改字符串
        for(int i= 0;i < 100;i++){
            new Thread(ne Runnable(){
                public void run(){
                    if(atomicReference.compareAndSet("abc","def")){
                        System.out.println(Thread.currrentThread().getName() + "把字符串abc更改为def");
                    }
                }
            })
        }

        //再创建100个线程
        for(int i= 0;i < 100;i++){
            new Thread(ne Runnable(){
                public void run(){
                    if(atomicReference.compareAndSet("def","abc")){
                        System.out.println(Thread.currrentThread().getName() + "把字符串def还原为abc");
                    }
                }
            })
        }
        
        Thread.sleep(1000);
        System.out.println(atomicReference.get());
    }
}

   AtomicReference可能会出现CAS的ABA问题。

public class Test{
    private static AtomicReference<String> atomicReference = new AtomicReference<>("abc");

    public static void main(String[] args){
        Thread t1 = new Thread(new Runnable(){
            public void run(){
                atomicReference.compareAndSet("abc","def");
                System.out.println(Thread.currentThread().getName() + "--" + atomicReference.get());
                atomicReference.compareAndSet("def","abc");
            }
        })

        Thread t2 = new Thread(new Runnable(){
            public void run(){
                Thread.sleep(1000);
                atomicReference.compareAndSet("abc","ghg");
            }
        })

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(atomicReference.get());
    }
}

   使用AtomicStampedReference、AtomicMarkedReference可以解决CAS中的ABA问题。在AtomicStampedReference原子类中有一个整数标记值stamp,每次执行CAS操作时,需要对比他的版本,即比较stamp的值。

public class Test{
    private static AtomicReference<String> atomicReference = new AtomicReference<>("abc");
    private static AtomicStampedReference<String> stampedReference = new AtomicStampedReference("abc",0);//版本号从0开始

    public static void main(String[] args){
        Thread t1 = new Thread(new Runnable(){
            public void run(){
                stampedReference.compareAndSet("abc","def",stampedReference.getSamp(),stampedReference.getSamp() + 1);
                System.out.println(Thread.currentThread().getName() + "--" + atomicReference.getReference());                
                stampedReference.compareAndSet("def","abc",stampedReference.getSamp(),stampedReference.getSamp() + 1);
            }
        })

        Thread t2 = new Thread(new Runnable(){
            public void run(){
                int stamp = stampedReference.getSamp();//先获得版本号
                Thread.sleep(1000);//在睡眠期间线程1已经进行了修改,版本号变了
                stampedReference.compareAndSet("abc","ghg",stamp,stamp + 1);//导致此时修改失败
            }
        })

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(stampedReference.getReference);
    }
}

PS:根据动力节点课程整理,如有侵权,联系删除。

这篇关于Java线程深入学习(二)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!