黑马程序员:Java培训、Android培训、iOS培训、.Net培训

        JAVA线程-内存模型和volatile详解

一、单核内存模型

   

    1、程序运行时,将临时数据存放到Cache中

    2、将CPU计算所需要的数据从Cache中拷贝一份到H Cache中

    3、CPU直接从H Cache中读取数据进行计算

    4、CPU将计算的结果写入H Cache中

    5、H Cache将最新的结果值涮入Cache中(何时写入不确定)

    6、将Cache中结果数据写回程序(如果有需要,例如文件、数据库)

    需要H Cache的原因:CPU的执行速度很快,而向Cache读取或写入数据则相对慢得多,因此,就需要H Cache来弥补。

二、多核内存模型

   

    有2个线程:ThreadA和ThreadB,分别在不同的CUP内运行,并且执行如下代码:

       i = 0; i = i + 1;。最后,我们希望i的值为2。

    1、ThreadA和ThreadB分别读取i=0的值存入各自的H Cache中,此时H Cache中的i值都为0;ThreadA和ThreadB分别对i进行计算并得到的结果都为1;2个H Cache分别将结果写入Cache,最终,Cache中i的值为1。显然,这不是我们希望得到的结果。这就是著名的:缓存一致性问题。(对单核CPU也会出现同样的问题,只是单核CPU以线程调度的形式来分别执行)

    2、缓存一致性问题的解决方法

       1)总线加LOCK#锁(效率低下,不可取)

       2)缓存一致性协议(这里不详述)

三、并发的三个概念

    1、原子性

       1)即一个操作或多个操作,要么全部执行并且执行的过程中不会被任何因素打断,要么不执行。

       2)有如下代码:i = 9999999999;

           假设为一个32位的变量赋值包括2个过程:为低16位赋值,为高16位赋值。现在,可能会发生:ThreadA将低16位数值写入之后,突然被中断,此时,ThreadB读取i的值就会得到错误的结果。

    2、可见性

       1)是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立即看到修改的值。

       2)ThreadA和ThreadB分别执行如下代码:

                       ThreadA :int i = 0;     

                                 i = 10;

                       ThreadB :int j = i;

         (1)当ThreadA执行到i=10时,首先将i的初始值加载到其CPU的H Cache中,然后赋值为10。那么,ThreadA的H Cache中i的值为10,但是,ThreadA却没有立即将H Cache中i的值马上写入Cache中。

         (2)此时,ThreadB开始执行,然而Cache中i的值仍然为0,最终,不管怎样,j的值都为0。而不是我们希望j=10那样。

         (3)这正是由于ThreadA对i修改后,ThreadB没有立即看到线程ThreadA对i修改的值。

3、有序性

   1)即程序执行的顺序按照代码的先后顺序执行。

   2)指令重排序:即处理器为了提高程序运行效率,可能会对输入代码进行优化,它不会保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

   3)有如下代码:

        int a = 10;  //语句1

        int r = 2;   //语句2

        a = a + 3;  //语句3   

        r = a * a;  //语句4

      可能执行的顺序:1 2 3 4 或 2 1 3 4,不再可能有其它。这是因为处理器需要考虑指令之间的数据依赖性。

   4)ThreadA和ThreadB分别执行如下代码:

          ThreadA:context = loadContext();  //语句1

                   inited = ture;           //语句2

          ThreadB:while(!inited) sleep();    //语句3

                   doSomething(context);   //语句4

      如果处理器对ThreadA的指令进行重排,则ThreadB可能在context没有赋值的情况下执行doSomething(context),从而导致程序运行错误。

    可见,原子性、可见性、有序性都不会影响单个线程对代码的执行结果。但是,会影并发执行的正确性。想要程序正确的并发执行,必须同时保证原子性、可见性、有序性。

四、Java确保并发执行的正确性

    1、原子性

       1)在Java中,对基本数据类型的变量的读取和赋值操作是原子性的。

       2)下面哪些语句是原子操作的

              x = 10;    //  是原子操作

              x = x + 1;  //  不是:首先读取x值,在加1,最后赋值

              x ++;      //  不是:首先读取x值,在加1,最后赋值

 

              y = x;    //   不是:首先读取x值,然后赋值

       3)如果实现更大范围的原子性,可使用synchronized和Lock来实现。

    2、可见性

       1)使用volatile来确保可见性:保证了修改的值会立即被更新到Cache并且使其它线程的H Cache中的volatile变量的值无效。

       2)也可使用synchronized和Lock来确保可见性:保证了修改的值在锁被释放之前被更新到Cache并且使其它线程的H Cache中的相关变量的值无效。

    3、有序性

       1)使用volatile来确保真正的有序性:禁止对volatile变量进行指令重排序。

       2)synchronized和Lock也能确保的有序性:以单线程执行同步代码块的方式来实现的。

五、volatile变量的详细论述

    1、volatile关键字的两层含义:

       1)保证可见性

       2)禁止指令重排序,保证有序性

         (1)当对volatile变量进行读取或写入操作时,在其前面对该volatile变量的操作早已完成并且结果已对当前操作可见,而当前操作的后序操作肯定还没有执行。

         (2)进行指令优化时,不能将操作volatile变量前的指令放到volatile变量后执行,也不能将操作volatile变量后的指令放到volatile变量前执行。

       注意:volatile关键字不保证原子性

      3)例如:

           x = 9;             //语句1

           y = 8;             //语句2

           volatile flag = ture;  //语句3

           i = 7;             //语句4

           k = 6;             //语句5

        尽管x,y,i,j之间不存在依赖性,但是,语句4和语句5不会被放到语句3之前执行,而语句1和语句2也不会被放到语句3之后执行。

        可能执行的顺序有:1,2,3,4,5 或 2,1,3,4,5,或 1,2,3,5,4

或2,1,3,5,4

    2、volatile与synchronized的区别

       1)共同点

         (1)volatile与synchronized都是同步机制的一部分

         (2)都实现了可见性和有序性

       2)区别

         (1)synchronized实现了原子性,而volatile没有

         (2)作用的对象不同:volatile作用的是变量,而synchronized作用的是语句块或方法。

         (3)对线程的作用不同:volatile不会阻塞线程,而synchronized会阻塞线程,即volatile没有使用监视器,而synchronized使用了监视器。所以,volatile是一种比synchronized更轻量的弱化的同步机制。

六、volatile的正确使用

    1、模式一:状态标记

       1)没有使用volatile导致的并发问题

           ThreadA :boolean stop = false;         ThreadB :stop = ture;

                     while(!stop){

                         //A

doSomething();

}

         并发可能不会正确执行:即ThreadA进入死循环

         原因:1、ThreadA永远只读其H Cache中stop的值

               2、ThreadB只将修改了stop的值保存到其H Cache中

3、即使ThreadA偶尔会从Cache中读取stop的值,如果Thread在A处阻

塞,而此时ThreadB修改了stop的值并且写入Cache,由于ThreadA没有看到Cache中stop值已经修改,即使重新执行,也可能会进入死循环。

      2)使用volatile解决

           ThreadA :boolean stop = false;         ThreadB :stop = ture;

                     while(!stop){

                         //A

doSomething();

}

    2、模式二:Double-check(在单例模式中的使用)

           private volatile static Singleton instance;

           public stratic Singleton getInstance(){

               if(instance == null){

                   synchronized(Singleton.class){

                       if(instance == null){ instance = new Singleton();}

                   }

               }

           }

        这是volatile与synchronized配合使用的经典案例。

    3、模式三:开销较低的读-写锁策略

           public class CheesyCounter{

              //All mutative operations msut be done with the ‘this’lock held

              @GuardedBy(“this”) private volatile int value;

              public int getVulue(){return value;}

              public synchronized int increment(){ return value++;}

          }

      1)volatile与synchronized配合使用的另一个经典案例

      2)如果读操作远远超过写操作,可以结合使用锁和volatile变量来减少公共代码路径的开销,例如本例。

      3)计数器必须使用synchronized来确保增量操作是原子的,同时使用volatile保证当前结果的可见性。

      4)如果更新不频繁,读取的开销仅仅涉及volatile操作,这由于一个无竞争锁获取的开销。

      网上有评论:本模式中value不使用volatile也能实现ThreadSave,因为increment()

使用了synchronized,真的这样吗?答案是否定的,如果ThreadA正进

行increment(),注意synchronized只实现了对increment()的互斥访问,而没有实现对value的互斥访问,而ThreadB也在进行getVulue(),那么ThreadB将会的到一个失效的value值,因为ThreadB不知道ThreadA正在对value进行修改。

4、模式四:一次性安全发布,发布不可变对象或线程安全的对象

          public class Test{

             public volatile FinalObject object;

             public void CreateObject(){object = new FinalObject(…);} //ThreadA

             public void doWork(){                            //ThreadB

                 while(ture){         //轮询

                    if(object != null){doSomething(object);}

                 }

             }

          }

       1)发布:使对象能够在当前线程作用域之外的代码中使用。

       2)如果object引用不是一个volatile型,doWork()对object的引用可能得到一个不完全构造的的FinalObject。

       3)必须注意:object本身必须是线程安全的。

       4)volatile类型确保了发布形式的可见型,但如果object的状态在发布后可变,那么就需要额外的同步。

       5)这个案例还展示出:在不使用阻塞的前提下,阻塞另一个线程的执行(尽管这不是真正的阻塞,但起到了阻塞的作用)。

       网上评论:volatile不足以实现安全发布,原因在于object在构造过程中可能被中断。我们应当记得volatile有一个很重要的特性:禁止对volatile变量进行指令重排序。即中断指令要么在object在构造前执行,要么在object在构造后执行,不可能出现在构造过程中。

    5、模式五:独立观察

          public class UserManager{

              public volatile String lastUser;

              public boolean authenticate(String userName, String password){

                  boolean valid = passwordIsValid(user, password);

                  if(valid){

                      User u = new User(userName, password);

                      activeUsers.add(u);

                      lasstUser = user;

                  }

                  return valid;

              }

              ………

          }

         1)本例展示了身份机制如何记忆最近一次登陆的用户的名字,并将反复使用lastUser引用来发布值,以供程序的其它部分使用。

         2)该模式的另一个使用是收集程序的统计信息或定期(不定期)发布信息

         3)这个模式要求:(1)被发布的值是有效不可变的-即值的状态在发布后不会更改(下一次发布已经是一个新的值,而不是在原有值的基础上的改变)

                         (2)使用发布值的代码需要清楚该值可能随时发生变化。

    6、模式六:volatile-bean模式

        @ThreadSafe

        public class Person{

            private volatile Stirng name;

            private volatile int age;

            public String getName(){return name;}

            public int getAge(){return age;}

            public void setName(String name){this.name = name;}

            public void setAge(int age){this.age = age;}

        }

        1)原理:很多框架为易变数据的持有者(例如HttpSession)提供了容器,但是放入这些容器中的对象必须是线程安全的。

        2)volatile-bean模式的所有成员都必须是volatile并且有效不可变,同时只能有getter和setter方法。

 

 

 

 

 

 

 

 

 

 

四、volatile变量的使用原则

    1、写入变量不依赖此变量的值,或只有一个线程修改此变量

    2、变量不与其它变量共同参与不变约束

    3、访问变量不需要加锁