java併發-內存模型與volatile

JMM的關鍵技術點都是圍繞着多線程的原子性、可見性和有序性來創建的。所以,咱們首先必須瞭解這些概念java

1,原子性編程

原子性是指一個操做是不可中斷的。即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其餘線程干擾,好比,對於一個靜態全局變量int i,兩個線程同時對它賦值,線程A給他賦值1,線程B給它賦值爲-1。那麼無論這2個線程以何種方式、何種步調工做,i的值要麼是1,要麼是-1。線程A和線程B之間是沒有干擾的。這就是原子性的一個特色,不可被中斷數組

2,可見性(Visibility)緩存

可見性是指當一個線程修改了某一個共享變量的值,其餘線程是否可以當即知道這個修改顯然,對於串行程序來講,可見性問題是不存在的。由於你在任何一個操做步驟中修改了某個變量,那麼在後續的步驟中,讀取這個變量的值,必定是修改後的新值。安全

3 有序性(Ordering)多線程

有序性問題多是三個問題中最難理解的了。對於一個線程的執行代碼而言,咱們老是習慣地認爲代碼的執行是從先日後,依次執行的。這麼理解也不能說徹底錯誤,由於就一個線程內而言,確實會表現成這樣。可是,在併發時,程序的執行可能就會出現亂序。給人直觀的感受就是:寫在前面的代碼,會在後面執行。聽起來有些難以想象,是嗎?有序性問題的緣由是由於程序在執行時,可能會進行指令重排,重排後的指令與原指令的順序未必一致併發

                                                                                                                   以上概念均摘自《實戰java高併發程序設計》ide

Java內存模型規定全部的變量都是存在主存當中,每一個線程都有本身的工做內存。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做。而且每一個線程不能訪問其餘線程的工做內存。高併發

主內存和工做內存

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節。此處的變量與Java編程時所說的變量不同,指包括了實例字段、靜態字段和構成數組對象的元素,可是不包括局部變量與方法參數,後者是線程私有的,不會被共享。性能

Java內存模型中規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存(能夠與前面講的處理器的高速緩存類比),線程的工做內存中保存了該線程使用到的變量到主內存副本拷貝,線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣線程之間沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要在主內存來完成,線程、主內存和工做內存的交互關係以下圖所示,和上圖很相似。

虛擬機內存交互關係

注意:這裏的主內存、工做內存與Java內存區域的Java堆、棧、方法區不是同一層次內存劃分,這二者基本上沒有關係。

內存交互操做

由上面的交互關係可知,關於主內存與工做內存之間的具體交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步到主內存之間的實現細節,Java內存模型定義瞭如下八種操做來完成:

  • lock(鎖定):做用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。
  • unlock(解鎖):做用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
  • read(讀取):做用於主內存變量,把一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
  • load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
  • use(使用):做用於工做內存的變量,把工做內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量的值的字節碼指令時將會執行這個操做。
  • assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦值給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
  • store(存儲):做用於工做內存的變量,把工做內存中的一個變量的值傳送到主內存中,以便隨後的write的操做。
  • write(寫入):做用於主內存的變量,它把store操做從工做內存中一個變量的值傳送到主內存的變量中。

若是要把一個變量從主內存中複製到工做內存,就須要按順尋地執行read和load操做,若是把變量從工做內存中同步回主內存中,就要按順序地執行store和write操做。Java內存模型只要求上述兩個操做必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是能夠插入其餘指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。Java內存模型還規定了在執行上述八種基本操做時,必須知足以下規則:

  • 不容許read和load、store和write操做之一單獨出現
  • 不容許一個線程丟棄它的最近assign的操做,即變量在工做內存中改變了以後必須同步到主內存中。
  • 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從工做內存同步回主內存中。
  • 一個新的變量只能在主內存中誕生,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操做以前,必須先執行過了assign和load操做。
  • 一個變量在同一時刻只容許一條線程對其進行lock操做,lock和unlock必須成對出現
  • 若是對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前須要從新執行load或assign操做初始化變量的值
  • 若是一個變量事先沒有被lock操做鎖定,則不容許對它執行unlock操做;也不容許去unlock一個被其餘線程鎖定的變量。
  • 對一個變量執行unlock操做以前,必須先把此變量同步到主內存中(執行store和write操做)。

關於volatile

Java 使用了一些特殊的操做或者關鍵字來申明、告訴虛擬機,在這個地方,要尤爲注意,不能隨意變更優化目標指令。關鍵字volatile 就是其中之一。當你用volatile 去申明一個變量時,就等於告訴了虛擬機,這個變量極有可能會被某些程序或者線程修改。爲了確保這個變量被修改後,應用程序範圍內的全部線程都可以「看到」這個改動,虛擬機就必須採用一些特殊的手段,保證這個變量的可見性等特色。

先來看一個使用場景,以下面一段代碼在32位java虛擬機上的運行

public class Test {
    public static long t = 0;

    public static class ChangeT implements Runnable {
        private long to;
        public ChangeT(long to) {
            this.to = to;
        }
        @Override
        public void run() {
            while (true) {
                Test.t = to;
                Thread.yield();
            }
        }
    }

    public static class ReadT implements Runnable {
        @Override
        public void run() {
            while (true) {
                long temp = Test.t;
                if (temp != 111L && temp != -999L && temp != 333L && temp != -444L) {
                    System.out.println(temp);
                }
                Thread.yield();
            }
        }
    }

    public static void main(String args[]) {
        new Thread(new ChangeT(111L)).start();
        new Thread(new ChangeT(-999L)).start();
        new Thread(new ChangeT(333L)).start();
        new Thread(new ChangeT(-444L)).start();
        new Thread(new ReadT()).start();
    }
}

能夠看出因爲long是64位在32位的虛擬機上經過多線程賦值,致使數據的讀和寫都不是原子性的,於是形成,打印出的數字部分是亂的

-4294966963
4294966297
-4294966963
-4294966963
4294966297
-4294967185
-4294966963
4294966297
...

但當使用 volatile修飾後就沒有問題了

public volatile static long t = 0;

 

同時也須要注意volatile是沒法徹底保證一些複合操做的原子性,它並不可以代替鎖。

package com.ishop.controller;

public class Test {
    public volatile static int i = 0;
    
    public static void main(String[] args) throws InterruptedException {
        AddThread t1 = new AddThread();
        AddThread t2 = new AddThread();
        AddThread t3 = new AddThread();
        AddThread t4 = new AddThread();
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        System.out.println("i="+i);

    }
}
class AddThread extends Thread {
    @Override
    public void run() {
        for (int a = 0; a < 100000; a++){
            Test.i++;
        }
    }
}

如:這一段代碼,輸出i老是一個小於100000的值。

對於volatile的特色以及使用場景總結以下:

volatile變量具備2種特性:

  • 保證變量的可見性。對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量最後的寫入,這個新值對於其餘線程來講是當即可見的。
  • 屏蔽指令重排序:指令重排序是編譯器和處理器爲了高效對程序進行優化的手段,下文有詳細的分析。

volatile語義並不能保證變量的原子性。對任意單個volatile變量的讀/寫具備原子性,但相似於i++、i–這種複合操做不具備原子性,由於自增運算包括讀取i的值、i值增長一、從新賦值3步操做,並不具有原子性。

因爲volatile只能保證變量的可見性和屏蔽指令重排序,只有知足下面2條規則時,才能使用volatile來保證併發安全,不然就須要加鎖(使用synchronized、lock或者java.util.concurrent中的Atomic原子類)來保證併發中的原子性。

  • 運算結果不存在數據依賴(重排序的數據依賴性),或者只有單一的線程修改變量的值(重排序的as-if-serial語義)
  • 變量不須要與其餘的狀態變量共同參與不變約束

由於須要在本地代碼中插入許多內存屏蔽指令在屏蔽特定條件下的重排序,volatile變量的寫操做與讀操做相比慢一些,可是其性能開銷比鎖低不少。

 

 

 

 

 

參考:《實戰java高併發程序設計》

文章部份內容引自:http://blog.csdn.net/u011080472/article/details/51337422

相關文章
相關標籤/搜索