沒有被正確同步的含義是什么
沒有正確同步的代碼對于不同的人來說可能會有不同的理解。在Java內存模型這個語義環境下,我們談到“沒有正確同步”,我們的意思是:
- 一個線程中有一個對變量的寫操作
- 另外一個線程對同一個變量有讀操作
- 而且寫操作和讀操作沒有通過同步來保證順序
當這些規則被違反的時候,我們就說這個變量上有一個“數據競爭(data race)”。一個數據競爭的程序就是一個沒有正確同步的程序。
同步有幾個方面的作用。最廣為人知的就是互斥:
一次只有一個線程能夠獲得一個監視器,因此,在一個監視器上面同步意味著一旦一個線程進入到監視器保護的同步塊中,其他的線程都不能進入到同一個監視器保護的塊中間,直到第一個線程退出了同步塊。
但是同步的含義比互斥更廣。同步保證了一個線程在同步塊之前或者在同步塊中的一個內存寫入操作以可預知的方式對其他相同監視器的線程可見。
- 當我們退出了同步塊,我們就釋放了這個監視器,這個監視器有刷新緩沖區到主內存的效果,因此該線程的寫入操作能夠為其他線程所見。
- 在我們進入一個同步塊之前,我們需要獲取監視器,監視器有使本地處理器緩存失效的功能,因此變量會從主存重新加載,于是其他線程對共享變量的修改對當前線程來說就變得可見了。
依據緩存來討論同步,可能聽起來這些觀點僅僅會影響到多處理器的系統。但是,重排序效果能夠在單一處理器上面很容易見到。對編譯器來說,在獲取之前或者釋放之后移動你的代碼是不可能的。當我們談到在緩沖區上面進行的獲取和釋放操作,我們使用了簡述的方式來描述大量可能的影響。
新的內存模型語義在內存操作(讀取字段,寫入字段,鎖,解鎖)以及其他線程的操作(start和join)中創建了一個部分排序,在這些操作中,一些操作被稱為在其他操作之前發生(where some actions are said to happen before other operations)。當一個操作在另外一個操作之前發生,第一個操作保證能夠排到前面并且對第二個操作可見。排序規則如下:
- 線程中的每個操作都發生在該線程中在線程順序上后續的每個操作之前(Each action in a thread happens before every action in that thread that comes later in the program's order)
- 解鎖一個監視器的操作發生在隨后對相同監視器進行鎖的操作之前(An unlock on a monitor happens before every subsequent lock on that same monitor.)
- 對volatile字段的寫操作發生在后續對相同volatile字段的讀取操作之前(A write to a volatile field happens before every subsequent read of that same volatile.)
- 線程上調用start()方法發生在這個線程啟動后的任何操作之前(A call to start() on a thread happens before any actions in the started thread.)
- 一個線程中所有的操作都發生在從這個線程join()方法成功返回的任何其他線程之前。(All actions in a thread happen before any other thread successfully returns from a join() on that thread.)(注:意思是其他線程等待一個線程的join()方法完成,那么,這個線程的所有操作發生在其他線程中的所有操作之前)
這意味著,任何內存操作:
- 這個內存操作在退出一個同步塊前對一個線程是可見的
- 對任何線程在它進入一個唄相同的監視器保護的同步塊后都是可見的
因為所有內存操作發生在釋放監視器之前和釋放監視器發生在獲取監視器之前
其他如下模式的實現被一些人用來強制實現一個內存屏障是沒有用的:
synchronized (new Object()){}
這段代碼其實不會執行任何操作,編譯器會把它完全移除掉,因為編譯器知道沒有其他的線程會使用相同的監視器進行同步。要看到其他線程的結果,你必須為一個線程建立happens before關系。
重點注意
對兩個線程來說,為了正確建立happends before關系而在相同監視器上面進行同步是非常重要的。以下觀點是錯誤的:當線程A在對象X上面同步的時候,所有東西對線程A可見,線程B在對象Y上面進行同步的時候,所有東西對線程B也是可見的。釋放監視器和獲取監視器必須匹配。(也就是說要在相同的監視器上面完成這兩個操作),否則,代碼就會存在“數據競爭”。