天天看點

多線程系列---記憶體模型JMM(一)

主記憶體和工作記憶體:

  Java記憶體模型的主要目标是定義程式中各個變量的通路規則,即在JVM中将變量存儲到記憶體和從記憶體中取出變量這樣的底層細節。此處的變量與Java程式設計裡面的變量有所不同步,它包含了執行個體字段、靜态字段和構成數組對象的元素,但不包含局部變量和方法參數,因為後者是線程私有的,不會共享,當然不存在資料競争問題(如果局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,但是reference引用本身在Java棧的局部變量表中,是線程私有的)。為了獲得較高的執行效能,Java記憶體模型并沒有限制執行引起使用處理器的特定寄存器或者緩存來和主記憶體進行互動,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。

  JMM規定了所有的變量都存儲在主記憶體(Main Memory)中。每個線程還有自己的工作記憶體(Working Memory),線程的工作記憶體中儲存了該線程使用到的變量的主記憶體的副本拷貝,線程對變量的所有操作(讀取、指派等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變量(volatile變量仍然有工作記憶體的拷貝,但是由于它特殊的操作順序性規定,是以看起來如同直接在主記憶體中讀寫通路一般)。不同的線程之間也無法直接通路對方工作記憶體中的變量,線程之間值的傳遞都需要通過主記憶體來完成。

多線程系列---記憶體模型JMM(一)

線程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位虛拟機中是非原子操作,資料有可能會被破壞,也就意味着多個線程在并發通路的時候是線程非安全的。

下面我們來示範這個32位JVM下,對64位long類型的資料的通路的問題:

public class Atomicity {
    //靜态變量t
    public  static long t = 0;
    //靜态變量t的get方法,同步方法
    public synchronized static long getT() {
        return t;
    }
    //靜态變量t的set方法,同步方法
    public synchronized static void setT(long t) {
        Atomicity.t = t;
    }
    //改變變量t的線程
    public static class ChangeT implements Runnable{
        private long to;
        public ChangeT(long to) {
            this.to = to;
        }
        public void run() {
            //不斷的将long變量設值到 t中
            while (true) {
                Atomicity.setT(to);
                //将目前線程的執行時間片段讓出去,以便由線程排程機制重新決定哪個線程可以執行
                Thread.yield();
            }
        }
    }
    //讀取變量t的線程,若讀取的值和設定的值不一緻,說明變量t的資料被破壞了,即線程不安全
    public static class ReadT implements Runnable{

        public void run() {
            //不斷的讀取NotAtomicity的t的值
            while (true) {
                long tmp = Atomicity.getT();
                //比較是否是自己設值的其中一個
                if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) {
                    //程式若執行到這裡,說明long類型變量t,其資料已經被破壞了
                    System.out.println(tmp);
                }
                将目前線程的執行時間片段讓出去,以便由線程排程機制重新決定哪個線程可以執行
                Thread.yield();
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new ChangeT(100L)).start();
        new Thread(new ChangeT(200L)).start();
        new Thread(new ChangeT(-300L)).start();
        new Thread(new ChangeT(-400L)).start();
        new Thread(new ReadT()).start();
    }
}
           

這樣做的話,可以保證對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關鍵字通過一個變量在同一時間隻允許有一個線程對其進行加鎖的規則來實作,

在單線程程式中,不會發生“指令重排”和“工作記憶體和主記憶體同步延遲”現象,隻在多線程程式中出現。

happens-before原則:

  Java記憶體模型中定義的兩項操作之間的次序關系,如果說操作A先行發生于操作B,操作A産生的影響能被操作B觀察到,“影響”包含了修改了記憶體中共享變量的值、發送了消息、調用了方法等。

  下面是Java記憶體模型下一些”天然的“happens-before關系,這些happens-before關系無須任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關系不在此列,并且無法從下列規則推導出來的話,它們就沒有順序性保障,虛拟機可以對它們進行随意地重排序。

  a.程式次序規則(Pragram Order Rule):在一個線程内,按照程式代碼順序,書寫在前面的操作先行發生于書寫在後面的操作。準确地說應該是控制流順序而不是程式代碼順序,因為要考慮分支、循環結構。

  b.管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生于後面對同一個鎖的lock操作。這裡必須強調的是同一個鎖,而”後面“是指時間上的先後順序。

  c.volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生于後面對這個變量的讀取操作,這裡的”後面“同樣指時間上的先後順序。

  d.線程啟動規則(Thread Start Rule):Thread對象的start()方法先行發生于此線程的每一個動作。

  e.線程終于規則(Thread Termination Rule):線程中的所有操作都先行發生于對此線程的終止檢測,我們可以通過Thread.join()方法結束,Thread.isAlive()的傳回值等作段檢測到線程已經終止執行。

  f.線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否有中斷發生。

  g.對象終結規則(Finalizer Rule):一個對象初始化完成(構造方法執行完成)先行發生于它的finalize()方法的開始。

  g.傳遞性(Transitivity):如果操作A先行發生于操作B,操作B先行發生于操作C,那就可以得出操作A先行發生于操作C的結論。

  一個操作”時間上的先發生“不代表這個操作會是”先行發生“,那如果一個操作”先行發生“是否就能推導出這個操作必定是”時間上的先發生 “呢?也是不成立的,一個典型的例子就是指令重排序。是以時間上的先後順序與happens-before原則之間基本沒有什麼關系,是以衡量并發安全問題一切必須以happens-before 原則為準。

繼續閱讀