• JAVA并发编程:相关概念及VOLATILE关键字解析


    一.内存模型的相关概念

      由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存即寄存器(工作内存)。

      这样在程序运行中先将运算的数据从主存中复制一份到高速缓存中,从而对高速缓存的数据进行读取、写入避免对内存的操作,完成后在将完成的数据更新到存中。

      概念:

        1.共享变量:多个线程会操作的变量

        2.缓存不一致:一个变量在多个CPU中缓存时,当各CPU从高速缓存中更新数据到主存时。

        缓存不一致的解决方案:    

        1)通过在总线加LOCK#锁的方式(一个线程访问时上锁,其他线程访问不了,效率低)。

        2)通过缓存一致性协议(最有代表性的是Inter的MESI协议,多线程访问时,当前线程操作共享变量时会发信号通知其他线程将该变量置为无效状态,当前线程将数据更新到主存后,其他线程再从主存更新,从而达到一致)。

        这2种方式都是硬件层面上提供的方式。

    二.并发编程中的三个概念

      1.原子性

      原子性:执行一个或多个操作,要么都成功,要么都失败。JVM中具有原子性的数据类型有8种基本类型中除了long和double以外的6种类型,加上64位操作系统和64位JVM中long和double是原子的,32位JVM中的long和double是非原子的。具体可参考:http://www.cnblogs.com/louiswong/p/5951895.html

      2.可见性

      可见性:多个线程操作一个变量时,其他线程能够立刻看到修改后的值。

      3.有序性

      有序性:程序执行的顺序由代码的先后顺序保证。

      指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。多线程访问时会出现重排序,单线程不会。多线程访问时如果代码前后有依赖时不会发生重排序,如:

    int a = 10;    //语句1
    int r = 2;    //语句2
    a = a + 3;    //语句3
    r = a*a;     //语句4

      上述例子中的语句4不会发生在语句3的前面。

      synchronized和lock能保证原子性、可见性和有序性,volatile只能保证可见性和有序性。此外除了这3种方式保证有序性外还有一种java先天有序性即先行发生原则,下面就来具体介绍下happens-before原则(先行发生原则):

      • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
      • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
      • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
      • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
      • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
      • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
      • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
      • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

      这8条原则摘自《深入理解Java虚拟机》。

      这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

      要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

    三.深入剖析volatile关键字

      1.volatile自身特性:

      • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
      • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

         为什么说volatile++这种复合操作不具有原子性?请看下面代码

    复制代码
    public class Test {
        public volatile int inc = 0;
         
        public void increase() {
            inc++;
        }
         
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
             
            while(Thread.activeCount()>1)  //保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }
    复制代码

      由于inc++自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入主存。那么就是说自增操作的三个子操作可能会分割开执行,假设这里有个线程1先读取了inc的值并且进行了加1操作,但是写入主存时阻塞了没有将其他线程的工作内存中的值置为无效,所以就会导致其他线程读到的还是未修改的值,最终结果就不一定是10000。

    2.volatile能保证有序性?

      在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

      volatile关键字禁止指令重排序有两层意思:

      1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

      2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

    复制代码
    //x、y为非volatile变量
    //flag为volatile变量
    x = 2;        //语句1
    y = 0;        //语句2
    flag = true;  //语句3
    x = 4;         //语句4
    y = -1;       //语句5
    复制代码

      volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的,但是不能保证1和2、4和5的顺序。

    3.volatile的原理和实现机制

      前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

      下面这段话摘自《深入理解Java虚拟机》:

      “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

      lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

      1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

      2)它会强制将对缓存的修改操作立即写入主存;

      3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

      下面通过具体事例说明:

    复制代码
    class VolatileBarrierExample {
        int a;
        volatile int v1 = 1;
        volatile int v2 = 2;
    
        void readAndWrite() {
            int i = v1;           //第一个volatile读
            int j = v2;           // 第二个volatile读
            a = i + j;            //普通写
            v1 = i + 1;          // 第一个volatile写
            v2 = j * 2;          //第二个 volatile写
        }
    
    }
    复制代码

      针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:

        

      上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。

      4.volatile的使用场景

    a.作为状态标志

      比如用于判断满足某个条件时执行某个事件,例如完成初始化或请求停机

    复制代码
    volatile boolean shutdownRequested;
    
    ...
    
    public void shutdown() { shutdownRequested = true; }
    
    public void doWork() { 
        while (!shutdownRequested) { 
            // do stuff
        }
    }
    复制代码

    b.一次性安全发布(double-check)

      在使用之前将检查这些数据是否曾经发布过

    复制代码
    class Singleton{
        private volatile static Singleton instance = null;
         
        private Singleton() {}
         
        public static Singleton getInstance() {
            if(instance==null) {
                synchronized (Singleton.class) {
                    if(instance==null)
                        instance = new Singleton();
                }
            }
            return instance;
        }
    }
    复制代码

    c.独立观察

      安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用,该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布期间不会更改,但是会需要多次发布。

    复制代码
    public class UserManager {
        public volatile String lastUser;
    
        public boolean authenticate(String user, String password) {
            boolean valid = passwordIsValid(user, password);
            if (valid) {
                User u = new User();
                activeUsers.add(u);
                lastUser = user;
            }
            return valid;
        }
    }
    复制代码

    d.volatile bean模式

      在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。

    复制代码
    @ThreadSafe
    public class Person {
        private volatile String firstName;
        private volatile String lastName;
        private volatile int age;
    
        public String getFirstName() { return firstName; }
        public String getLastName() { return lastName; }
        public int getAge() { return age; }
    
        public void setFirstName(String firstName) { 
            this.firstName = firstName;
        }
    
        public void setLastName(String lastName) { 
            this.lastName = lastName;
        }
    
        public void setAge(int age) { 
            this.age = age;
        }
    }
    复制代码

    e.开销较低的读写锁策略

      当读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。清单 6 中显示的线程安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

    参考自:http://www.cnblogs.com/longshiyVip/p/5173877.html

        http://www.cnblogs.com/jiangds/p/6251245.html

        http://www.ibm.com/developerworks/cn/java/j-jtp06197.html 

    一.内存模型的相关概念

      由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存即寄存器(工作内存)。

      这样在程序运行中先将运算的数据从主存中复制一份到高速缓存中,从而对高速缓存的数据进行读取、写入避免对内存的操作,完成后在将完成的数据更新到存中。

      概念:

        1.共享变量:多个线程会操作的变量

        2.缓存不一致:一个变量在多个CPU中缓存时,当各CPU从高速缓存中更新数据到主存时。

        缓存不一致的解决方案:    

        1)通过在总线加LOCK#锁的方式(一个线程访问时上锁,其他线程访问不了,效率低)。

        2)通过缓存一致性协议(最有代表性的是Inter的MESI协议,多线程访问时,当前线程操作共享变量时会发信号通知其他线程将该变量置为无效状态,当前线程将数据更新到主存后,其他线程再从主存更新,从而达到一致)。

        这2种方式都是硬件层面上提供的方式。

    二.并发编程中的三个概念

      1.原子性

      原子性:执行一个或多个操作,要么都成功,要么都失败。JVM中具有原子性的数据类型有8种基本类型中除了long和double以外的6种类型,加上64位操作系统和64位JVM中long和double是原子的,32位JVM中的long和double是非原子的。具体可参考:http://www.cnblogs.com/louiswong/p/5951895.html

      2.可见性

      可见性:多个线程操作一个变量时,其他线程能够立刻看到修改后的值。

      3.有序性

      有序性:程序执行的顺序由代码的先后顺序保证。

      指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。多线程访问时会出现重排序,单线程不会。多线程访问时如果代码前后有依赖时不会发生重排序,如:

    int a = 10;    //语句1
    int r = 2;    //语句2
    a = a + 3;    //语句3
    r = a*a;     //语句4

      上述例子中的语句4不会发生在语句3的前面。

      synchronized和lock能保证原子性、可见性和有序性,volatile只能保证可见性和有序性。此外除了这3种方式保证有序性外还有一种java先天有序性即先行发生原则,下面就来具体介绍下happens-before原则(先行发生原则):

      • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
      • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
      • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
      • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
      • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
      • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
      • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
      • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

      这8条原则摘自《深入理解Java虚拟机》。

      这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

      要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

    三.深入剖析volatile关键字

      1.volatile自身特性:

      • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
      • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

         为什么说volatile++这种复合操作不具有原子性?请看下面代码

    复制代码
    public class Test {
        public volatile int inc = 0;
         
        public void increase() {
            inc++;
        }
         
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
             
            while(Thread.activeCount()>1)  //保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }
    复制代码

      由于inc++自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入主存。那么就是说自增操作的三个子操作可能会分割开执行,假设这里有个线程1先读取了inc的值并且进行了加1操作,但是写入主存时阻塞了没有将其他线程的工作内存中的值置为无效,所以就会导致其他线程读到的还是未修改的值,最终结果就不一定是10000。

    2.volatile能保证有序性?

      在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

      volatile关键字禁止指令重排序有两层意思:

      1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

      2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

    复制代码
    //x、y为非volatile变量
    //flag为volatile变量
    x = 2;        //语句1
    y = 0;        //语句2
    flag = true;  //语句3
    x = 4;         //语句4
    y = -1;       //语句5
    复制代码

      volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的,但是不能保证1和2、4和5的顺序。

    3.volatile的原理和实现机制

      前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

      下面这段话摘自《深入理解Java虚拟机》:

      “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

      lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

      1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

      2)它会强制将对缓存的修改操作立即写入主存;

      3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

      下面通过具体事例说明:

    复制代码
    class VolatileBarrierExample {
        int a;
        volatile int v1 = 1;
        volatile int v2 = 2;
    
        void readAndWrite() {
            int i = v1;           //第一个volatile读
            int j = v2;           // 第二个volatile读
            a = i + j;            //普通写
            v1 = i + 1;          // 第一个volatile写
            v2 = j * 2;          //第二个 volatile写
        }
    
    }
    复制代码

      针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:

        

      上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。

      4.volatile的使用场景

    a.作为状态标志

      比如用于判断满足某个条件时执行某个事件,例如完成初始化或请求停机

    复制代码
    volatile boolean shutdownRequested;
    
    ...
    
    public void shutdown() { shutdownRequested = true; }
    
    public void doWork() { 
        while (!shutdownRequested) { 
            // do stuff
        }
    }
    复制代码

    b.一次性安全发布(double-check)

      在使用之前将检查这些数据是否曾经发布过

    复制代码
    class Singleton{
        private volatile static Singleton instance = null;
         
        private Singleton() {}
         
        public static Singleton getInstance() {
            if(instance==null) {
                synchronized (Singleton.class) {
                    if(instance==null)
                        instance = new Singleton();
                }
            }
            return instance;
        }
    }
    复制代码

    c.独立观察

      安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用,该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布期间不会更改,但是会需要多次发布。

    复制代码
    public class UserManager {
        public volatile String lastUser;
    
        public boolean authenticate(String user, String password) {
            boolean valid = passwordIsValid(user, password);
            if (valid) {
                User u = new User();
                activeUsers.add(u);
                lastUser = user;
            }
            return valid;
        }
    }
    复制代码

    d.volatile bean模式

      在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。

    复制代码
    @ThreadSafe
    public class Person {
        private volatile String firstName;
        private volatile String lastName;
        private volatile int age;
    
        public String getFirstName() { return firstName; }
        public String getLastName() { return lastName; }
        public int getAge() { return age; }
    
        public void setFirstName(String firstName) { 
            this.firstName = firstName;
        }
    
        public void setLastName(String lastName) { 
            this.lastName = lastName;
        }
    
        public void setAge(int age) { 
            this.age = age;
        }
    }
    复制代码

    e.开销较低的读写锁策略

      当读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。清单 6 中显示的线程安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

  • 相关阅读:
    Java(八)——面向对象(4)-抽象类与接口
    Java(七)——面向对象(3)-多态
    Java(六)——面向对象(2)-继承
    Java(五)——面向对象(1)-基础
    Java(四)——数组
    Java(三)——流程控制
    Java(二)——Java基础
    易忘小技巧--yum
    网络测速命令--speedtest
    大型网站架构技术读后感
  • 原文地址:https://www.cnblogs.com/wuyuxin/p/7001579.html
Copyright © 2020-2023  润新知