原文地址:http://ifeve.com/easy-happens-before/html
學習Java併發,到後面總會接觸到happens-before偏序關係。初接觸玩意兒簡直就是不知所云,下面是通過一段時間折騰後我的對此的一點淺薄理解,但願對初接觸的人有幫助。若有不正確之處,歡迎指正。java
synchronized、大部分鎖,衆所周知的一個功能就是使多個線程互斥/串行的(共享鎖容許多個線程同時訪問,如讀鎖)訪問臨界區,但他們的第二個功能 —— 保證變量的可見性 —— 常被遺忘。api
爲何存在可見性問題?簡單介紹下。相對於內存,CPU的速度是極高的,若是CPU須要存取數據時都直接與內存打交道,在存取過程當中,CPU將一直空閒,這是一種極大的浪費,媽媽說,浪費是很差的,因此,現代的CPU裏都有不少寄存器,多級cache,他們比內存的存取速度高多了。某個線程執行時,內存中的一份數據,會存在於該線程的工做存儲中(working memory,是cache和寄存器的一個抽象,這個解釋源於《Concurrent Programming in Java: Design Principles and Patterns, Second Edition》§2.2.7,原文:Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. 有很多人以爲working memory是內存的某個部分,這多是有些譯做將working memory譯爲工做內存的緣故,爲避免混淆,這裏稱其爲工做存儲,每一個線程都有本身的工做存儲),並在某個特定時候回寫到內存。單線程時,這沒有問題,若是是多線程要同時訪問同一個變量呢?內存中一個變量會存在於多個工做存儲中,線程1修改了變量a的值何時對線程2可見?此外,編譯器或運行時爲了效率能夠在容許的時候對指令進行重排序,重排序後的執行順序就與代碼不一致了,這樣線程2讀取某個變量的時候線程1可能尚未進行寫入操做呢,雖然代碼順序上寫操做是在前面的。這就是可見性問題的由來。數組
咱們沒法枚舉全部的場景來規定某個線程修改的變量什麼時候對另外一個線程可見。但能夠制定一些通用的規則,這就是happens-before。它是一個偏序關係,Java內存模型中定義了許多Action,有些Action之間存在happens-before關係(並非全部Action兩兩之間都有happens-before關係)。「ActionA happens-before ActionB」這樣的描述很擾亂視線,是否是?OK,換個描述,若是ActionA happens-before ActionB,咱們能夠記做hb(ActionA,ActionB)或者記做ActionA < ActionB,這貨在這裏已經不是小於號了,它是偏序關係,是否是隱約有些離散數學的味道,不喜歡?嗯,我也不喜歡,so,下面都用hb(ActionA,ActionB)這種方式來表述。多線程
從Java內存模型中取兩條happens-before關係來瞅瞅:併發
- An unlock on a monitor happens-before every subsequent lock on that monitor.
- A write to a volatile field happens-before every subsequent read of that volatile.
「對一個monitor的解鎖操做happens-before後續對同一個monitor的加鎖操做」、「對某個volatile字段的寫操做happens-before後續對同一個volatile字段的讀操做」……莫名其妙、不知所云、不能理解……就是這個心情。是否是說解鎖操做要先於鎖定操做發生?這有違常規啊。確實不是這麼理解的。happens-before規則不是描述實際操做的前後順序,它是用來描述可見性的一種規則,下面我給上述兩條規則換個說法:oracle
- 若是線程1解鎖了monitor a,接着線程2鎖定了a,那麼,線程1解鎖a以前的寫操做都對線程2可見(線程1和線程2能夠是同一個線程)。
- 若是線程1寫入了volatile變量v(這裏和後續的「變量」都指的是對象的字段、類字段和數組元素),接着線程2讀取了v,那麼,線程1寫入v及以前的寫操做都對線程2可見(線程1和線程2能夠是同一個線程)。
是否是很簡單,瞬間以爲這篇文章弱爆了,說了那麼多,其實就是在說「若是hb(a,b),那麼a及以前的寫操做在另外一個線程t1進行了b操做時都對t1可見(同一個線程就不會有可見性問題,下面再也不重複了)」。雖然弱爆了,但還得善始善終,是否是,繼續來,再看兩條happens-before規則:app
- All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
- Each action in a thread happens-before every subsequent action in that thread.
通俗版:性能
- 線程t1寫入的全部變量(全部action都與那個join有hb關係,固然也包括線程t1終止前的最後一個action了,最後一個action及以前的全部寫入操做,因此是全部變量),在任意其它線程t2調用t1.join()成功返回後,都對t2可見。
- 線程中上一個動做及以前的全部寫操做在該線程執行下一個動做時對該線程可見(也就是說,同一個線程中前面的全部寫操做對後面的操做可見)
大體都是這個樣子的解釋。學習
happens-before關係有個很重要的性質,就是傳遞性,即,若是hb(a,b),hb(b,c),則有hb(a,c)。
Java內存模型中只是列出了幾種比較基本的hb規則,在Java語言層面,又衍生了許多其餘happens-before規則,如ReentrantLock的unlock與lock操做,又如AbstractQueuedSynchronizer的release與acquire,setState與getState等等。
接下來用hb規則分析兩個實際的可見性例子。
- 看個CopyOnWriteArrayList的例子,代碼中的list對象是CopyOnWriteArrayList類型,a是個靜態變量,初始值爲0
假設有如下代碼與執行線程:
那麼,線程2中b的值會是1嗎?來分析下。假設執行軌跡爲如下所示:
線程1 線程2
p1:a = 1
p2:list.set(1,"t")
p3:list.get(2)
p4:int b = a;
p1,p2是同一個線程中的,p3,p4是同一個線程中的,因此有hb(p1,p2),hb(p3,p4),要使得p1中的賦值操做對p4可見,那麼只須要有hb(p1,p4),前面說過,hb關係具備傳遞性,那麼如有hb(p2,p3)就能獲得hb(p1,p4),p2,p3是否是存在hb關係?翻翻javaapi,發現有以下描述:
Actions in a thread prior to placing an object into any concurrent collection happen-before actions subsequent to the access or removal of that element from the collection in another thread.
p2是放入一個元素到併發集合中,p3是從併發集合中取,符合上述描述,所以有hb(p2,p3).也就是說,在這樣一種執行軌跡下,能夠保證線程2中的b的值是1.若是是下面這樣的執行軌跡呢?
線程1 線程2
p1:a = 1
p3:list.get(2)
p2:list.set(1,"t")
p4:int b = a;
依然有hb(p1,p2),hb(p3,p4),可是沒有了hb(p2,p3),得不到hb(p1,p4),雖然線程1給a賦值操做在執行順序上是先於線程2讀取a的,但jmm不保證最後b的值是1.這不是說必定不是1,只是不能保證。若是程序裏沒有采起手段(如加鎖等)排除相似這樣的執行軌跡,那麼是沒法保證b取到1的。像這樣的程序,就是沒有正確同步的,存在着數據爭用(data race)。
既然提到了CopyOnWriteArrayList,那麼順便看下其set實現吧:
01 |
public E set( int index, E element) { |
02 |
final ReentrantLock lock = this .lock; |
05 |
Object[] elements = getArray(); |
06 |
Object oldValue = elements[index]; |
08 |
if (oldValue != element) { |
09 |
int len = elements.length; |
10 |
Object[] newElements = Arrays.copyOf(elements, len); |
11 |
newElements[index] = element; |
12 |
setArray(newElements); |
有意思的地方是else裏的setArray(elements)調用,看看setArray作了什麼:
1 |
final void setArray(Object[] a) { |
一個簡單的賦值,array是volatile類型。elements是從getArray()方法取過來的,getArray()實現以下:
1 |
final Object[] getArray() { |
也很簡單,直接返回array。取得array,又從新賦值給array,有甚意義?setArray(elements)上有條簡單的註釋,但可能不是太容易明白。正如前文提到的那條javadoc上的規定,放入一個元素到併發集合與從併發集合中取元素之間要有hb關係。set是放入,get是取(取還有其餘方法),怎麼才能使得set與get之間有hb關係,set方法的最後有unlock操做,若是get裏有對這個鎖的lock操做,那麼就好知足了,可是get並無加鎖:
1 |
public E get( int index) { |
2 |
return (E)(getArray()[index]); |
可是get裏調用了getArray,getArray裏有讀volatile的操做,只須要set走任意代碼路徑都能遇到寫volatile操做就能知足條件了,這裏主要就是if…else…分支,if裏有個setArray操做,若是隻是從單線程角度來講,else裏的setArray(elements)是沒有必要的,可是爲了使得走else這個代碼路徑時也有寫volatile變量操做,就須要加一個setArray(elements)調用。
最後,以FutureTask結尾,這應該是個比較有名的例子了,隨提一下。提交任務給線程池,咱們能夠經過FutureTask來獲取線程的運行結果。絕大部分時候,將結果寫入FutureTask的線程和讀取結果的不會是同一個線程。寫入結果的代碼以下:
13 |
if (compareAndSetState(s, RAN)) { |
獲取結果的代碼以下:
1 |
V innerGet( long nanosTimeout) throws InterruptedException, ExecutionException, TimeoutException { |
2 |
if (!tryAcquireSharedNanos( 0 , nanosTimeout)) |
3 |
throw new TimeoutException(); |
4 |
if (getState() == CANCELLED) |
5 |
throw new CancellationException(); |
7 |
throw new ExecutionException(exception); |
結果就是result變量,但result不是volatile變量,而這裏有沒有加鎖操做,那麼怎麼保證寫入到result的值對讀取result的線程可見?這裏是通過精心設計的,由於讀寫volatile的開銷很小,但畢竟仍是存在開銷的,且做爲一個基礎類庫,追求最後一點性能也不爲過,由於沒法預知全部可能的使用場景。這裏主要利用了AbstractQueuedSynchronizer中的releaseShared與tryAcquireSharedNanos存在hb關係。
線程1: 線程2:
p1:result = v;
p2:releaseShared(0);
p3:tryAcquireSharedNanos(0, nanosTimeout)
p4:return result;
正如前面分析的那樣,在這個執行軌跡中,有hb(p1,p2),hb(p3,p4)且有hb(p2,p3),全部有hb(p1,p4),所以,即便result是普通變量,p1中的寫操做也是對p4可見的。但,會不會存在這樣的軌跡呢:
線程1: 線程2:
p1:result = v;
p3:tryAcquireSharedNanos(0, nanosTimeout)
p2:releaseShared(0);
p4:return result;
這也是一個關鍵點所在,這種狀況是決計不會發生的。由於若是沒有p2操做,那麼p3在執行tryAcquireSharedNanos時會一直被阻塞,直到releaseShared操做執行了或超過了nanosTimeout超時時間或被中斷拋出InterruptedException,如果releaseShared執行了,則就變成了第一個軌跡,如果超時,那麼返回值是false,代碼邏輯中就直接拋出了異常,不會去取result了,因此,這個地方設計的很精巧。這就是所謂的「捎帶同步(piggybacking on synchronization)」,即,沒有特地爲result變量的讀寫設置同步,而是利用了其餘同步動做時「捎帶」的效果。但在咱們本身寫代碼時,應該儘量避免這樣的作法,由於,很差理解,對編碼人員要求高,維護難度大。
本文只是簡單地解釋了下hb規則,文中還出現了許多名詞沒有作更多介紹,爲啥沒介紹?介紹開來就是一本書啦,他們就是《Java Memory Model》、《Java Concurrency in Practice》、《Concurrent Programming in Java: Design Principles and Patterns》等,這些書裏找定義與解釋吧。