多線程面試題之原子性、可見性、有序性

面試官:「對java併發瞭解怎麼樣?」 java

應聘者:「還能夠…」  面試

面試官:「爲了保證線程安全,Java併發有哪幾個基本特性呢?」  安全

應聘者:「有三條基本性質,原子性、可見性、有序性」  多線程

面試官:  「具體解釋下這三個特性?」  併發

應聘者:「bala。bala。bala。。」 app

Java內存模型是圍繞着併發過程當中如何處理原子性、可見性、有序性這三個特徵來創建的,下面是這三個特性的實現原理:原子性(Atomicity)eclipse

由Java內存模型來直接保證的原子性變量操做包括read、load、use、assign、store和write六個,大體能夠認爲基礎數據類型的訪問和讀寫是具有原子性的。若是應用場景須要一個更大範圍的原子性保證,Java內存模型還提供了lock和unlock操做來知足這種需求,儘管虛擬機未把lock與unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱匿地使用這兩個操做,這兩個字節碼指令反映到Java代碼中就是同步塊—synchronized關鍵字,所以在synchronized塊之間的操做也具有原子性。ide

Java中的原子操做包括:

1)除long和double以外的基本類型的賦值操做 
2)全部引用reference的賦值操做 
3)java.concurrent.Atomic.* 包中全部類的一切操做。this

可是java對long和double的賦值操做是非原子操做!long和double佔用的字節數都是8,也就是64bits。在32位操做系統上對64位的數據的讀寫要分兩步完成,每一步取32位數據。這樣對double和long的賦值操做就會有問題:若是有兩個線程同時寫一個變量內存,一個進程寫低32位,而另外一個寫高32位,這樣將致使獲取的64位數據是失效的數據。所以須要使用volatile關鍵字來防止此類現象。volatile自己不保證獲取和設置操做的原子性,僅僅保持修改的可見性。可是java的內存模型保證聲明爲volatile的long和double變量的get和set操做是原子的。編碼

public class UnatomicLong implements Runnable {    private static long test = 0;    private final long val;    public UnatomicLong(long val) {        this.val = val;    }    @Override    public void run() {        while (!Thread.interrupted()) {            test = val; //兩個線程都試圖將本身的私有變量val賦值給類私有靜態變量test        }    }    public static void main(String[] args) {        Thread t1 = new Thread(new UnatomicLong(-1));        Thread t2 = new Thread(new UnatomicLong(0));        System.out.println(Long.toBinaryString(-1));        System.out.println(pad(Long.toBinaryString(0), 64));        t1.start();        t2.start();        long val;        while ((val = test) == -1 || val == 0) {        //若是靜態成員test的值是-1或0,說明兩個線程操做沒有交叉        }        System.out.println(pad(Long.toBinaryString(val), 64));        System.out.println(val);        t1.interrupt();        t2.interrupt();    }    // prepend 0s to the string to make it the target length    private static String pad(String s, int targetLength) {        int n = targetLength - s.length();        for (int x = 0; x < n; x++) {            s = "0" + s;        }        return s;    }}

運行發現程序在while循環時進入了死循環,這是由於使用的JVM是64bits。在64位JVM中double和long的賦值操做是原子操做。 
在eclipse中修改jre爲一個32bit的JVM地址,則會有以下運行結果:

1111111111111111111111111111111111111111111111111111111111111111 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000011111111111111111111111111111111 //很明顯test的值被破壞了 4294967295

可見性(Visibility)

可見性就是指當一個線程修改了線程共享變量的值,其它線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方法來實現可見性的,不管是普通變量仍是volatile變量都是如此,普通變量與volatile變量的區別是volatile的特殊規則保證了新值能當即同步到主內存,以及每使用前當即從內存刷新。由於咱們能夠說volatile保證了線程操做時變量的可見性,而普通變量則不能保證這一點。

除了volatile以外,Java還有兩個關鍵字能實現可見性,它們是synchronized。同步塊的可見性是由「對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store和write操做)」這條規則得到的,而final關鍵字的可見性是指:被final修飾的字段是構造器一旦初始化完成,而且構造器沒有把「this」引用傳遞出去,那麼在其它線程中就能看見final字段的值。

Lock也能夠保證可見性,由於它能夠保證任一時刻只有一個線程能訪問共享資源,並在其釋放鎖以前將修改的變量刷新到內存中。有序性(Ordering)

Java內存模型中的程序自然有序性能夠總結爲一句話:若是在本線程內觀察,全部操做都是有序的;若是在一個線程中觀察另外一個線程,全部操做都是無序的。前半句是指「線程內表現爲串行語義」,後半句是指「指令重排序」現象和「工做內存主主內存同步延遲」現象。

Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由「一個變量在同一時刻只容許一條線程對其進行lock操做」這條規則來得到的,這個規則決定了持有同一個鎖的兩個同步塊只能串行地進入。

先行發生原則:

若是Java內存模型中全部的有序性都只靠volatile和synchronized來完成,那麼有一些操做將會變得很囉嗦,可是咱們在編寫Java併發代碼的時候並無感受到這一點,這是由於Java語言中有一個「先行發生」(Happen-Before)的原則。這個原則很是重要,它是判斷數據是否存在競爭,線程是否安全的主要依賴。

先行發生原則是指Java內存模型中定義的兩項操做之間的依序關係,若是說操做A先行發生於操做B,其實就是說發生操做B以前,操做A產生的影響能被操做B觀察到,「影響」包含了修改了內存中共享變量的值、發送了消息、調用了方法等。它意味着什麼呢?以下例:

//線程A中執行i = 1;//線程B中執行j = i;//線程C中執行i = 2;

假設線程A中的操做」i=1「先行發生於線程B的操做」j=i「,那麼咱們就能夠肯定在線程B的操做執行後,變量j的值必定是等於1,結出這個結論的依據有兩個,一是根據先行發生原則,」i=1「的結果能夠被觀察到;二是線程C登場以前,線程A操做結束以後沒有其它線程會修改變量i的值。如今再來考慮線程C,咱們依然保持線程A和B之間的先行發生關係,而線程C出如今線程A和B操做之間,可是C與B沒有先行發生關係,那麼j的值多是1,也多是2,由於線程C對應變量i的影響可能會被線程B觀察到,也可能觀察不到,這時線程B就存在讀取到過時數據的風險,不具有多線程的安全性。

下面是Java內存模型下一些」自然的「先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,能夠在編碼中直接使用。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對它們進行隨意地重排序。

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的結論。

一個操做」時間上的先發生「不表明這個操做會是」先行發生「,那若是一個操做」先行發生「是否就能推導出這個操做一定是」時間上的先發生「呢?也是不成立的,一個典型的例子就是指令重排序。因此時間上的前後順序與先生髮生原則之間基本沒有什麼關係,因此衡量併發安全問題一切必須以先行發生原則爲準。

相關文章
相關標籤/搜索