Java内存模型与volatile

内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节。

 

每一个线程有一块工作内存区,其中保留了被所有线程共享的主内存中的变量的值的拷贝。为了存取一个共享的变量,一个线程通常先获取锁定并且清除它的工作内存区,这保证该共享变量从所有线程的共享内存区正确地装入到线程的工作内存区,当线程解锁时保证该工作内存区中变量的值写回到共享内存中。

 

下面简单给出了规则的重要推论:
1、 适当运用同步结构,能够正确地把一个或一组值通过共享变量从一个线程传送到另一个线程。
2、 当一个线程使用一变量的值时,它获取的值其实是由它本身或其它线程在变量中存储的值。即使用程序中没有使用正确的同步也是如此。例如,如果两个线程把不同对象的引用存储到同一个共享引用变量中,那么该引用变量值将要么是这个线程、要么是那个线程所拥有的对象的引用的值,而该共享引用变量的值不可能由多个线程引用值组合而成(对于共享基本类型long、double除外)。
3、 在java应用程序中,如果没有给出明确的同步,那么可以以一个令人惊奇的自由地更新主内存内容。所以Java程序开发者如果想避免这种情况,那么应该使用明确的同步技术。

 

一个变量是Java程序可以存取的一个地址,它不仅包括基本类型变量、引用类型变量,而且还包括数组类型变量。保存在主内存区的变量可以被所有线程共享,但一个线程存取另一个线程的参数或局部变量是不可能的,所以我们不必担心这些变量的线程安全性问题。

 

每一个线程有一个工作内存区,其中保留了它必须使用或赋值的变量的一个自己的工作拷贝。当线程执行时,它在这些工作内存区的拷贝上操作。主内存中包含的是每个变量的主拷贝,当允许线程(或线程需要,如读)把它的工作拷贝中的内容写回主内存中(或者反之)时,是有一些规则对此加以限制的,这些规则在后面会提到。

 

一个线程可以执行的动作有使用(use)、赋值(assing)、装载(load)、存储(store)、锁定(lock)、解锁(unlock),而主内存可以执行的动作有read、write、lock、unlock,每一个这样的动作都是原子的。下面是JMM内存模式图:


使用(use)和赋值(assing)操作是线程的执行引擎(为上图中的Thread Execution Engine)和线程的工作内存(为上图中的Working Memory)之间紧密藕合(直接,即一步就可以完成的)的交互过程;锁定(lock)和解锁(unlock)操作是线程的执行引擎和主内存之间紧密藕合的交互过程;但在主内存和线程的工作内存间的数据传送是松散藕合的,当数据从主内存复制到工作存储时,必须出现两个动作:由主内存执行的读(read)动作,一段时间后是由工作内存执行的相应的load动作;当数据从工作内存拷贝到主内存时,必须出现两种动作:由工作内存执行的存储(store)动作,一段时间后是由主内存执行的相应的写(write)动作。在主内存和工作内存间传送数据需一定的传送时间,而且对每次的传送的传送时间可能是不同的:因此在另一个线程看来,线程对不同变量所执行的动作可能是按照不同的顺序(与程序代码语义顺序)执行的(比如说,线程内的程序代码是先给变量a赋值,再给变量b赋值,而在另一线程看来有可能先看见主内存中的b变量更新,再看见a变量更新),然而,任何一个线程在主内存中对一个变量的所有动作一定是按照这个线程中所有对该变量动作的相同次(也是指与程序代码语议的顺序)序执行。

 

 

每种操作的详细定义:

  • 线程的use动作把一个变量的线程工作拷贝的内容传送给线程执行引擎。每当线程执行一个用到变量的值的虚拟机指令时执行这个动作。
  • 线程的assign动作把一个值从线程执行引擎传送到变量的线程工作拷贝。每当线程执行一个给变量赋值的虚拟机指令时执行这个动作。
  • 主内存的read动作把一个变量的主内存拷贝的内容传输到线程的工作内存以便后面的load动作使用。
  • 线程的load动作把read动作从主内存中得到的值放入变量的线程工作拷贝中。
  • 线程的store动作把一个变量的线程工作拷贝内容传送到主内存中以便后面的write动作使用。
  • 主内存的write动作把store动作从线程工作内存中得到的值放入主内存中一个变量的主拷贝。
  • 和主内存紧密同步的线程的lock动作使线程获得一个独占锁定的声明。
  • 和主内存紧密同步的线程的unlock动作使线程释放一个独占锁定的声明。

这样,线程和变量的相互作用由use、assign、load和store动作的序列组成。主内存为每个load动作执行read动作,为每个Store动作执行write动作。线程的锁定的相互作用由lock或unlock动作顺序组成。

 

 

线程的每个load动作有唯一一个主内存的read动作和它相匹配,这个load动作跟在read动作的后面;线程的每个store动作有唯一一个主内存的write动作和它相匹配,这个write动作跟在store动作的后面。

 

变量规则:不允许一个线程丢弃它的最近的assign操作;不允许一个线程无原因地把数据从线程的工作内存写回到主内存中;一个新的变量只能在主内存中产生并且不能在任何线程的工作内存中初始化。

 

假设动作A是线程T对变量V执行的另外的load或store动作,假设动作P是主内存对变量V执行的相应的read或write动作。类似地,假设动作B是线程T对同一个变量V执行的另外的load或store动作,假设动作Q是主内存对变量V执行的相应的read或write动作。如果A等于B,那么必须有P先于Q。(不很严格地:为了一个线程,主内存执行对给定的一个变量的主拷贝动作必须遵循线程执行时要求的先后顺序。)注意,这条规则只适用于一个线程对于同一个变量不同动作的情况,是针对单线程提出的。然而,对于volatile 类型的变量有更严格的规则,请看后面volatile变量规则最后一条。

 

double和long类型变量的非原子处理:如果一个double或者long变量没有声明为volatile ,则变量的read或write动作,实际在主内存处理时是把它当作两个32位的read或write动作,这两个动作在时间上是分开的,可能会有其它的动作介于它们之间。这样的结果是,如果两个并发的线程对共享的非volatile 类型的double或long变量赋不同的值,那么随后对该变量的使用而获取的值可能不等于任何一个线程所赋的值,而可能是依赖于具体应用的两个线程所赋的值的混合。基于目前32芯片技术,在共享double和long变量时必须同步。

 

在一个时刻,对同一个锁,只能有一个线程拥有它,而且一个线程可以对同一个锁执行多次lock动作,只有当对这个锁执行相同次数的unlock动作后,线程才会释放该锁定。

 

一个线程如果没有拥有锁,那么它不允许对该锁实施unlock动作。

 

如果一个线程对任何一个锁定实施unlock,线程必须先把它工作内存中的赋的值写回到主内存中(即unlock动作会引发对变量的store -> write -> unlock 动作序列)。

 

一个lock动作发生时会清空线程工作内存中所有变量,所以在使用它们的时候必须从主内存中载入或重新赋值(即lock动作会引发对变量的lock -> read -> load 或lock -> assign -> store动作序列)。

 

 

volatile 类型变量的规则:如果一个变量声明为volatile 类型,那么每个线程对该变量实施的动作有以下附加的规则,假定T表示一个线程,V,W表示volatile 类型变量:

  • 只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对就是V的use动作可以认为是和线程T对变量V的load动作相应的read动作相关联(这样可以保证看其他线程对变量V所做的修改后的值,即使用时先去从主内存中加载)。
  • 只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对就是V的assign动作可以认为是和线程T对变量V的store动作相应的write动作相关联(这样可以保证其他线程可以看到自己对变量V所做的修改,即修改后写回主内存中)。
  • 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(不严格地:为了一个线程T,主内存实施对给定的volatile变量的主拷贝的动作必须遵循和线程执行时要求的一样的先后顺序。也即将V,W变量写回到主内存的顺序与程序代码行对V,W赋值先后顺序一样;线程将V,W变量从主内存读取出来的顺序与程序代码行对V,W使用先后顺序一样。即volate禁止了变量间的重新排序问题)。该规则进一步加强了多线程访问共享变量的安全性,这条规则是针对多线程提出的。

对声明为volatile 的变量的规则有效地保证了:线程对一个声明为volatile 的变量的每个use或assign动作只要访问主内存一次,并且依照线程的执行语义所指定的次序访问主内存,然而,对没有声明为volatile 的变量的read或write动作,这样的内存动作是没有次序限制的。

 

 

volatile 的变量除了具有可见性外,还禁止了多个变量间的Reordering。

 

范例:可能的交换

Java代码  
  1. class Sample{  
  2.     int a=1,b=2;  
  3.     void hither(){  
  4.         a=b;  
  5.     }  
  6.       
  7.     void yon(){  
  8.         b=a;  
  9.     }  
  10. }  

让我们考虑调用hither的线程,按照规则,该线程必须执行变量b的use动作,在它后面要执行变量a的assign动作,这是对hither的最低要求(即同一线程内一定是按照程序语义顺序来执行)。
现在线程对变量b的第一个动作不能为use,但是可以为assign或load。这里对b的一个assign动作不可能发生,因为这里根本就没有赋值调用,所以这里只有对变量b的load动作。而线程对这个load动作必须有一个更早的主内丰对变量b的read动作。
在对变量a进行assign动作后,线程可选地(因为没有使用同步)存储变量a的值,如果线程要存储这个值,那么线程实施store动作,并且主内存接着实施变量a的write动作。
调用方法yon的线程的情况是类似的,只是a和b交换了各自的角色。所有的动作序列可以由下面图描述,运行时可能会从这些任意的箭头中切换到另一线程执行:


 

假定ha和hb是调用hither的线程的变量a和b的工作拷贝,假定ya和yb是调用yon线程的变量a和b的工作拷贝,假定ma和mb是主内存中变量a和变量b的主拷贝,假定初始化ma=1,mb=2,下面是动作的可能结果:
1、 ha=2,hb=2,ya=2,yb=2,ma=2,mb=2(结果是b拷贝给了a)
2、 ha=1,hb=1,ya=1,yb=1,ma=1,mb=1(结果是a拷贝给了b)
3、 ha=2,hb=2,ya=1,yb=1,ma=2,mb=1(结果是a、b交换了)
使用以下程序进行测试:

 

Java代码  
  1. class Sample {  
  2.     /* 
  3.      * 不管 a,b是否使用volatile 修饰,都会出现 a、b值交换。因为a=b、b=a并不是原子性 
  4.      * 的,因为这两条语句都会涉及到使用与赋值两个动作,完全有可能在访问操作后切换到 
  5.      * 另一线程,而volatile并不像synchronized那样具有原子特性 
  6.      */  
  7.     volatile int a = 1;  
  8.     volatile int b = 2;  
  9.   
  10.     void hither() {  
  11.         a = b;  
  12.     }  
  13.   
  14.     synchronized void yon() {  
  15.         b = a;  
  16.     }  
  17. }  
  18.   
  19. public class Test {  
  20.     public static void main(String[] args) throws Exception {  
  21.         while (!Thread.currentThread().isInterrupted()) {  
  22.             final Sample s = new Sample();  
  23.             final Thread hither = new Thread() {  
  24.                 public void run() {  
  25.                     s.hither();  
  26.                 }  
  27.             };  
  28.             final Thread yon = new Thread() {  
  29.                 public void run() {  
  30.                     s.yon();  
  31.                 }  
  32.             };  
  33.             hither.start();  
  34.             yon.start();  
  35.             new Thread() {  
  36.                 public void run() {  
  37.                     try {  
  38.                         hither.join();  
  39.                         yon.join();  
  40.                     } catch (InterruptedException e) {  
  41.                         e.printStackTrace();  
  42.                     }  
  43.                     if (s.a != s.b) {  
  44.                         // 某次打印结果Thread-332984: a=2 b=1  
  45.                         System.out.println(this.getName() + ": a=" + s.a + " b=" + s.b);  
  46.                         System.exit(0);  
  47.                     }  
  48.                 }  
  49.   
  50.             }.start();  
  51.             Thread.yield();  
  52.         }  
  53.     }  
  54. }  

上面使用volatile 同时修改这两个变量还是不行的,除非两个方法同时(注,只一个方法使用也是不管用的)使用synchronized:

Java代码  
  1. class Sample {  
  2.     int a = 1;  
  3.     int b = 2;  
  4.   
  5.     synchronized void hither() {  
  6.         a = b;  
  7.     }  
  8.     synchronized void yon() {  
  9.         b = a;  
  10.     }  
  11. }  

 

lock和unlock动作对主内存的动作次序提出了更多的限制。在一个线程的lock动作和unlock动作之间,另一个线程不能实 施lock动作,而且,unlock动作前需要实施store动作和write动作,下面是仅可能发现的顺序,从结果看出要么是a,要么 是b,不可能出现两都交换的情况:
1、 ha=2,hb=2,ya=2,yb=2,ma=2,mb=2(结果是b拷贝给了a)
2、 ha=1,hb=1,ya=1,yb=1,ma=1,mb=1(结果是a拷贝给了b)

 

范例:无序写入
下面的例子和前面的例子很相似,只是一个方法对两个变量赋值,而中一个方法读取两个变量的值:

Java代码  
  1. class Sample {  
  2.     int a = 1;  
  3.     int b = 2;  
  4.     String result;  
  5.   
  6.     synchronized void to() {  
  7.         a = 3;  
  8.         b = 4;  
  9.     }  
  10.   
  11.     void fro() {  
  12.         // 按理来说不可能出现 a=1,b=4  
  13.         result = "a=" + a + ",b=" + b;  
  14.     }  
  15. }  



 
从上图可以看出,在线程内:调用方法to的线程在方法结束而实施unlock动作前,必须实施stroe动作将所赋的值写回到主内存中。调用方法fro的线程必须同样的次序使用变量a和b(即先use a再use b),并且必须从主内存对变量a和b实施load动作以将值装入a和b。
在主内存中:动作发生的次序是这样的呢?注意规则并不要求对变量a的write动作要先于对变量b的write动作;而且也不要求对变量a的read动作要先于对变量b的read动作。甚至由于方法to是同步的,方法fro没有同步,所以不能防止在lock动作和unlock动作间发生read动作(即,声明一个方法为同步的,这种机制本身不能使方法的行为是原子的)。
上面结果输出有可能是a=1,b=4,这说明尽管一个线程对变量a的assign动作先于变量b的assign动作,在另一个线程看来,主内存实施可能是按照相反的次序实施相应的write动作,但如果是volatile变量,则会以程序语义执行的顺序写回主内存。

 

 

什么是重新排序?
在一些情况下,对程序变量(对象实例变量、静态变量、数组元素)进行访问的时候,会发现访问的执行顺序与程序中所指定的顺序并不一致。只要不改变程序的语义,编译器为了进行程序的优化可以自由地reorder指令(instructions)。处理器也可能以不同的顺序去执行指令:数据可能会以不同于程序中所指定的顺序在处理器寄存器、处理器缓存以及住内存之间移动。

 

在单线程的情况下,程序不必去关注指令的真实执行顺序,同时也不必在意reordering的影响。然而,在多线程的情况下,如果程序没有被正确地synchronized,线程就会受到reordering的影响,即一个线程可能会看到另一个线程对变量访问过程的次序与程序中指定的次序不同的结果。例如,如果一个线程先写入a字段,然后再写入b字段,如果b的值不依赖于a的值,则编译器可以自由地recorder这些操作,而且可以将缓存中b的值先写回到主内存中再写回a,这样另一线程会先看到b,最后该线程看到的结果与程序中指定的次序不同。

 

可能进行reorder的地方包括:编译器、JIT、处理器缓存。

 

JMM 允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员已经使用 synchronized 或 final 明确地请求了某些可见性保证。这意味着在缺乏同步的情况下,从不同的线程角度来看,内存的操作是以不同的次序发生的。

 

例子:

Java代码  
  1. Class Reordering {     
  2.   int x = 0, y = 0;     
  3.   public void writer() {     
  4.     x = 1;     
  5.     y = 2;     
  6.   }     
  7.     
  8.   public void reader() {     
  9.     int r1 = y;     
  10.     int r2 = x;     
  11.   }     
  12. }  

假设有两个线程分别执行上面的示例程序代码中的writer和reader方法。在writer方法的程序中,x被指定在y之前进行赋值。但是由于对y的赋值并不依赖于x的值,因此编译器可以reorder这些操作。另外,执行writer线程的处理器也完全可以先将cache中y的值写回内存,然后再将x的值写回内存。在两个操作的中间(y的值已经被写回内存,但是x的值尚未被写回内存的时候),执行reader方法的线程得到的结果便是 r1 = 2,但是 r2 = 0,而不是x的真实值1。

 

Volatile关键字规则:
volatile字段被用来在线程之间communicate state(交流规则)。任意线程所read的volatile字段的值都是最新的。原因有以下有4点:
(1) 编译器和JVM会阻止将volatile字段的值放入处理器寄存器(register); 
(2) 在write volatile字段之后,其值会被flush出处理器cache,写回memory;

(3) 在read volatile字段之前,会invalidate(验证)处理器cache。

因此,上述两条便保证了每次read的值都是从memory中的,即具有“可见性”这一特性。 
(4) 禁止reorder(重排序,即与原程序指定的顺序不一致)任意两个volatile变量,并且同时严格限制(尽管没有禁止)reorder volatile变量周围的非volatile变量。这一点即volatile具有变量的“顺序性”,即指令不会重新排序,而是按照程序指定的顺序执行。 

:在旧的内存模型下,对volatile修改的变量的访问顺序不能进行重新排序,但可以对非volatile变量进行排序,但这样又可能还是会导致volatile变量可见性问题,所以老的旧内存模型没有从根本上解决volatile 变量的可见性问题。在新的内存模型下,仍然是不允许对volatile变量进行reorder的,不同的是再也不轻易(虽然没有完全禁止掉)允许对它周围的非volatile变量进行排序。
 
由于第(4)条中对volatile字段以及周围非volatile字段(或变量)reorder的限制,如下程序中,假设线程A 正在执行reader方法,同时,线程B正在执行writer方法。线程B完成对volatile字段 v 的赋值后,相应的结果被写回内存。如果此时线程 A 便得到的 v 的值正好为true,那么线程A也可以安全地引用 x 的值。然而,需要注意的是,假如v不是volatile的,那么上述结果就不一定了,因为x和v赋值的顺序可能被reorder。

Java代码  
  1. class VolatileSample1 {  
  2.     int x = 0;  
  3.     volatile boolean v = false;  
  4.   
  5.     public void writer() {  
  6.         x = 42;  
  7.         v = true;  
  8.     }  
  9.   
  10.     public void reader() {  
  11.         /* 由于volatile的特点,这里要想v为true,则x的肯定已经执行赋值(assign)动作 
  12.          * 且已写回(writer)主内了,所以不会出现 v=true,x=0的情形。但时,如果这里先 
  13.          * 访问的是x变量,则由于volatile不具有原子性,则还是会出现v=true,x=0的情形, 
  14.          * 具体请看后面测试 
  15.          */  
  16.         if (v == true) {  
  17.             //uses x - 确保能看见42.     
  18.         }  
  19.     }  
  20. }  

假设一个线程调用writer,一个线程调用reader。写线程将V写回到主内存中,读线程从主内存中获取v。因此,如果读的线程能够看到v的值为true,这就能确保该读线程能看到x的值为42,因为x是在v前面赋值,所以也会先写回到内存。如果v不是volatile,编译器就可能在写回到主内存时对v与x进行reorder,这样的就可能在读的线程看到v为true时,x却还是为0,因为写线程对写回主内存动作进行重新reoder过了。

 

下面是对volatile变量的测试:

Java代码  
  1. class VolatileSample2 {  
  2.     int x = 0;  
  3.     volatile boolean v = false;  
  4.     String result;  
  5.   
  6.     public void writer() {  
  7.         x = 42;  
  8.         v = true;  
  9.     }  
  10.   
  11.     public void reader() {  
  12.         /* 
  13.          * 如果是 result="x="+x+",v="+v;,则快就会出现 x=0,v=true 这样的结果,因为这 
  14.          * 条语 句完全可能先访问x后,另外线程再执行writer方法,待writer方法执行完成后 
  15.          * ,再接着访问v,此时就会出现 x=0,v=true 的结果; 
  16.          *  
  17.          * 但如果是result="v="+v+",x="+x;,则要想出现 v=true,x=0 的结果,则一定要等 
  18.          * writer方法执行完并写回到主内存后再执行reader方法,由于v声明的是volatile变 
  19.          * 量,volatile变量会禁止reorder任意两个者volatile变量,并且同时严格限制 
  20.          * reorder volatile变量周围 的非 volatile变量,所以由于x 比v前赋值,则写回主 
  21.          * 存时也会一定按照此顺序,所以当v为true时,则主存中的x肯定是42,绝不会是0( 
  22.          * 这里不要注意的是,volatile的变量也会在读取主内存时严格按照程序的顺序执行, 
  23.          * 所以这里根本不会先访问x再v的可能,如果这那样,则也会出现v=true,x=0的结果)。 
  24.          *  
  25.          * 这里只将x声明成volatile其结果也是一样的。当然如果两个都声明成volatile时, 
  26.          * 会更安全,因为这里会完全“禁止”重排,而一个的话只是“严格限制”而已,可能还是 
  27.          * 不会很安全,所以一般 两个都设置为最安全。 
  28.          *  
  29.          * 当x,v都是volatile时, result="x="+x+",v="+v;执行的结果还是有可能为 
  30.          * x=0,v=true, 因为volatile只是保证了可见性与顺序性两个特点为,但并不能保证 
  31.          * 原子性。此种情况下要得到 x=0,v=true,只需reader方法先执行,等访问x完后而v 
  32.          * 还未访问时,开始调试writer方法, 待writer整个方法执行完后并将x,v写回主内存 
  33.          * 后,再执行reader方法,继续访问v,此时的结 果就是x=0,v=true。 另外, 
  34.          * result="x="+x+",v="+v;也会严格安照程序的顺序来执行访问操作(即volatile不 
  35.          * 只是在写回内存时是按程序语义的执行顺序来执行,在读的时候也是这样 要按照程序 
  36.          * 的访问顺序来,但如果不是volatile变量时,则read动作就可能不会按照程序顺序来 
  37.          * 执行,但这好像对纯粹的访问操作没有什么影响,这好只有访问操作的不变对象一样 
  38.          * ,不会出现线程不安全的问题),即先访问x后再能访问v,但这绝不是原子性的,很 
  39.          * 有可能从他们中 间 切换到其他线程。 
  40.  
  41.          * 另外,在测试的过程中发现writer方法的原子性要比reader的原子性要强,即多个访 
  42.          * 问操作在 一起不如多个赋值语句原子性强 
  43.          */  
  44.         result = "x=" + x + ",v=" + v;  
  45.         //result = "v=" + v + ",x=" + x;  
  46.     }  
  47.   
  48. }  
  49.   
  50. public class VolatileTest {  
  51.     public static void main(String[] args) {  
  52.         while (!Thread.currentThread().isInterrupted()) {  
  53.             final VolatileSample2 s = new VolatileSample2();  
  54.             final Thread w = new Thread(){  
  55.                 public void run() {  
  56.                     s.writer();  
  57.                 }  
  58.             };  
  59.               
  60.             final Thread r = new Thread(){  
  61.                 public void run() {  
  62.                     s.reader();  
  63.                 }  
  64.             };  
  65.             r.start();  
  66.             w.start();  
  67.             new Thread(){  
  68.                 public void run() {  
  69.                     try {  
  70.                         w.join();  
  71.                         r.join();  
  72.                     } catch (InterruptedException e) {  
  73.                         e.printStackTrace();  
  74.                     }  
  75.                     if (s.result.equals("x=0,v=true")) {  
  76.                         System.out.println(this.getName() + " " + s.result);  
  77.                         System.exit(0);  
  78.                     }  
  79.                 }  
  80.             }.start();  
  81.   
  82.             Thread.yield();  
  83.         }  
  84.     }  
  85. }  

 

双重检测在新的内存模型下能很好地工作吗?

Java代码  
  1. // double-checked-locking - don‘t do this!  
  2.   
  3. private static Something instance = null;  
  4.   
  5. public Something getInstance() {  
  6.   if (instance == null) {  
  7.     synchronized (this) {  
  8.       if (instance == null)  
  9.         instance = new Something();//1  
  10.     }  
  11.   }  
  12.   return instance;  
  13. }  

首先,如果上面程序不加任何修改,这个在旧的或是新的内存模型下都不能正确的工作。上面程序 //1 处在多线程的情况下会有问题,如果一个线程在 //1 处已调用完构造器,但Something的实例域可能还没有被写回到内存,而在这之前会将创建好的对象(但并非初始化完全的对象,因为没有将它的实例域完全写回到主内存)赋值给了instance引用,这样另一个线程拿到的instance所指向的对象其实是不完整的,即所指向的对象的实例域还不可见,这样在使用这个instnace时就会有问题。在1.5或之后的版本中,我们可以将instance设置为volatile就可以了,这样就会确保将实例域的数据写回到主内存的动作在将实例赋值给instance引用动作之前发生(即volatile的 happens-before 规则),所以这样就确保了在使用前对象已完全初始化完成。

 

 

在新的内存模型下怎么才能使用final域正常的工作呢?

JSR 133新的目标中提出了一个初始化安全的新保障:如果一个对象被安全、适当的构造(在构造器中将当前正在构造的对象this暴露给外界是不安全的,“安全构造”技术请参考这里),这样其他线程可以在不使用同步的情况下看到该对象在构造器里设置的final域的值。

在构造期间,不要公布“this”引用,即在构造函数完成之前,使 this 引用暴露给另一个线程,这种暴露可能是显示的,也可能是隐式的。
 

Java代码  
  1. class FinalFieldExample {  
  2.   final int x;  
  3.   int y;  
  4.   static FinalFieldExample f;  
  5.   public FinalFieldExample() {  
  6.     x = 3;  
  7.     y = 4;  
  8.   }  
  9.   
  10.   static void writer() {  
  11.     f = new FinalFieldExample();  
  12.   }  
  13.   
  14.   static void reader() {  
  15.     if (f != null) {  
  16.       int i = f.x;  
  17.       int j = f.y;  
  18.     }  
  19.   }  
  20. }  

上面的类中展示了怎么使用final域。能够确保另一调用 reader 的线程它能看到f.x的值是因为f.x是final的,但不能保证它能看到f.y的值是4,因为它不是final的。如果FinalFieldExample的构造器像这样:

Java代码  
  1. public FinalFieldExample() { // bad!  
  2.   x = 3;  
  3.   y = 4;  
  4.   // bad construction - allowing this to escape   
  5.   global.obj = this;// 暴露this  
  6. }  

这样其他线程通过global.obj读x的值将不能确保是3。
上面列举的例子是final类型的基本类型变量,如果final修饰的是一个引用类型,则也会有这样的保障:在拿到final引用类型前这个引用所指向的对象的所有域将完全初始化构造完成。

 

 

-----

happens-before规则:这里

 

“原始 JMM 的缺点”请看这里

 

JSR 133新的目标可以看这里。最主要的是第6点,它进一步说明了提高初始化安全的保障:即使在没有使用同步的情况下,也能看到由其他线程在构造器中对final域所赋的值。

 

旧的内存模型下,“问题1:String对象不可变对象不是不可变的”请参见这里。不过这里还是要说明一下的就是,旧的内存模型是允许的这样的,有此JVM还出现过这样的问题,不过在新的Java内存模型中是不合法,即不会再出这种问题。从1.5中的代码我们也可以看出,它在1.4源码的基本上将value[]、offset、count定义成了final,这样在新的内存模型下通过final避免了这个问题。

 

旧的内存模型下,“问题 2:重新排序volatile和非volatile变量”请参见这里

Java内存模型与volatile,古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。