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關鍵字經過一個變量在同一時間只容許有一個線程對其進行加鎖的規則來實現,

在單線程程序中,不會發生「指令重排」和「工做內存和主內存同步延遲」現象,只在多線程程序中出現。

相關文章
相關標籤/搜索