java内存模型

主内存和工做内存:编程

  Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不一样步,它包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量和方法参数,由于后者是线程私有的,不会共享,固然不存在数据竞争问题(若是局部变量是一个reference引用类型,它引用的对象在Java堆中可被各个线程共享,可是reference引用自己在Java栈的局部变量表中,是线程私有的)。为了得到较高的执行效能,Java内存模型并无限制执行引发使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。数组

  JMM规定了全部的变量都存储在主内存(Main Memory)中。每一个线程还有本身的工做内存(Working Memory),线程的工做内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的全部操做(读取、赋值等)都必须在工做内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工做内存的拷贝,可是因为它特殊的操做顺序性规定,因此看起来如同直接在主内存中读写访问通常)。不一样的线程之间也没法直接访问对方工做内存中的变量,线程之间值的传递都须要经过主内存来完成。缓存

                                                                   

 

  线程1和线程2要想进行数据的交换通常要经历下面的步骤:安全

  1.线程1把工做内存1中的更新过的共享变量刷新到主内存中去。多线程

  2.线程2到主内存中去读取线程1刷新过的共享变量,而后copy一份到工做内存2中去。并发

 

 Java内存模型是围绕着并发编程中原子性、可见性、有序性这三个特征来创建的,那咱们依次看一下这三个特征:函数

  原子性(Atomicity):一个操做不能被打断,要么所有执行完毕,要么不执行。在这点上有点相似于事务操做,要么所有执行成功,要么回退到执行该操做以前的状态。优化

  基本类型数据的访问大都是原子操做,long 和double类型的变量是64位,可是在32位JVM中,32位的JVM会将64位数据的读写操做分为2次32位的读写操做来进行,这就致使了long、double类型的变量在32位虚拟机中是非原子操做,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的。this

下面咱们来演示这个32位JVM下,对64位long类型的数据的访问的问题:线程

复制代码

1 public class NotAtomicity {
 2     //静态变量t
 3     public  static long t = 0;
 4     //静态变量t的get方法
 5     public  static long getT() {
 6         return t;
 7     }
 8     //静态变量t的set方法
 9     public  static void setT(long t) {
10         NotAtomicity.t = t;
11     }
12     //改变变量t的线程
13     public static class ChangeT implements Runnable{
14         private long to;
15         public ChangeT(long to) {
16             this.to = to;
17         }
18         public void run() {
19             //不断的将long变量设值到 t中
20             while (true) {
21                 NotAtomicity.setT(to);
22                 //将当前线程的执行时间片断让出去,以便由线程调度机制从新决定哪一个线程能够执行
23                 Thread.yield();
24             }
25         }
26     }
27     //读取变量t的线程,若读取的值和设置的值不一致,说明变量t的数据被破坏了,即线程不安全
28     public static class ReadT implements Runnable{
29 
30         public void run() {
31             //不断的读取NotAtomicity的t的值
32             while (true) {
33                 long tmp = NotAtomicity.getT();
34                 //比较是不是本身设值的其中一个
35                 if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) {
36                     //程序若执行到这里,说明long类型变量t,其数据已经被破坏了
37                     System.out.println(tmp);
38                 }
39                 ////将当前线程的执行时间片断让出去,以便由线程调度机制从新决定哪一个线程能够执行
40                 Thread.yield();
41             }
42         }
43     }
44     public static void main(String[] args) {
45         new Thread(new ChangeT(100L)).start();
46         new Thread(new ChangeT(200L)).start();
47         new Thread(new ChangeT(-300L)).start();
48         new Thread(new ChangeT(-400L)).start();
49         new Thread(new ReadT()).start();
50     }
51 }

复制代码

咱们建立了4个线程来对long类型的变量t进行赋值,赋值分别为100,200,-300,-400,有一个线程负责读取变量t,若是正常的话,读取到的t的值应该是咱们赋值中的一个,可是在32的JVM中,事情会出乎预料。若是程序正常的话,咱们控制台不会有任何的输出,可实际上,程序一运行,控制台就输出了下面的信息:

-4294967096
4294966896
-4294967096
-4294967096
4294966896
之因此会出现上面的状况,是由于在32位JVM中,64位的long数据的读和写都不是原子操做,即不具备原子性,并发的时候相互干扰了。

  32位的JVM中,要想保证对long、double类型数据的操做的原子性,能够对访问该数据的方法进行同步,就像下面的:

复制代码

1 public class Atomicity {
 2     //静态变量t
 3     public  static long t = 0;
 4     //静态变量t的get方法,同步方法
 5     public synchronized static long getT() {
 6         return t;
 7     }
 8     //静态变量t的set方法,同步方法
 9     public synchronized static void setT(long t) {
10         Atomicity.t = t;
11     }
12     //改变变量t的线程
13     public static class ChangeT implements Runnable{
14         private long to;
15         public ChangeT(long to) {
16             this.to = to;
17         }
18         public void run() {
19             //不断的将long变量设值到 t中
20             while (true) {
21                 Atomicity.setT(to);
22                 //将当前线程的执行时间片断让出去,以便由线程调度机制从新决定哪一个线程能够执行
23                 Thread.yield();
24             }
25         }
26     }
27     //读取变量t的线程,若读取的值和设置的值不一致,说明变量t的数据被破坏了,即线程不安全
28     public static class ReadT implements Runnable{
29 
30         public void run() {
31             //不断的读取NotAtomicity的t的值
32             while (true) {
33                 long tmp = Atomicity.getT();
34                 //比较是不是本身设值的其中一个
35                 if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) {
36                     //程序若执行到这里,说明long类型变量t,其数据已经被破坏了
37                     System.out.println(tmp);
38                 }
39                 ////将当前线程的执行时间片断让出去,以便由线程调度机制从新决定哪一个线程能够执行
40                 Thread.yield();
41             }
42         }
43     }
44     public static void main(String[] args) {
45         new Thread(new ChangeT(100L)).start();
46         new Thread(new ChangeT(200L)).start();
47         new Thread(new ChangeT(-300L)).start();
48         new Thread(new ChangeT(-400L)).start();
49         new Thread(new ReadT()).start();
50     }
51 }

复制代码

这样作的话,能够保证对64位数据操做的原子性。

 

 可见性:一个线程对共享变量作了修改以后,其余的线程当即可以看到(感知到)该变量这种修改(变化)。

  Java内存模型是经过将在工做内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工做内存中,这种依赖主内存的方式来实现可见性的。

不管是普通变量仍是volatile变量都是如此,区别在于:volatile的特殊规则保证了volatile变量值修改后的新值马上同步到主内存,每次使用volatile变量前当即从主内存中刷新,所以volatile保证了多线程之间的操做变量的可见性,而普通变量则不能保证这一点。

  除了volatile关键字能实现可见性以外,还有synchronized,Lock,final也是能够的。

  使用synchronized关键字,在同步方法/同步块开始时(Monitor Enter),使用共享变量时会从主内存中刷新变量值到工做内存中(即从主内存中读取最新值到线程私有的工做内存中),在同步方法/同步块结束时(Monitor Exit),会将工做内存中的变量值同步到主内存中去(即将线程私有的工做内存中的值写入到主内存进行同步)。

  使用Lock接口的最经常使用的实现ReentrantLock(重入锁)来实现可见性:当咱们在方法的开始位置执行lock.lock()方法,这和synchronized开始位置(Monitor Enter)有相同的语义,即便用共享变量时会从主内存中刷新变量值到工做内存中(即从主内存中读取最新值到线程私有的工做内存中),在方法的最后finally块里执行lock.unlock()方法,和synchronized结束位置(Monitor Exit)有相同的语义,即会将工做内存中的变量值同步到主内存中去(即将线程私有的工做内存中的值写入到主内存进行同步)。

  final关键字的可见性是指:被final修饰的变量,在构造函数数一旦初始化完成,而且在构造函数中并无把“this”的引用传递出去(“this”引用逃逸是很危险的,其余的线程极可能经过该引用访问到只“初始化一半”的对象),那么其余线程就能够看到final变量的值。

  有序性:对于一个线程的代码而言,咱们老是觉得代码的执行是从前日后的,依次执行的。这么说不能说彻底不对,在单线程程序里,确实会这样执行;可是在多线程并发时,程序的执行就有可能出现乱序。用一句话能够总结为:在本线程内观察,操做都是有序的;若是在一个线程中观察另一个线程,全部的操做都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工做内存和主内存同步延迟”现象。

Java提供了两个关键字volatile和synchronized来保证多线程之间操做的有序性,volatile关键字自己经过加入内存屏障来禁止指令的重排序,而synchronized关键字经过一个变量在同一时间只容许有一个线程对其进行加锁的规则来实现,

在单线程程序中,不会发生“指令重排”和“工做内存和主内存同步延迟”现象,只在多线程程序中出现。

相关文章
相关标签/搜索