1. 程式人生 > >同步和Java記憶體模型 (三)可見性

同步和Java記憶體模型 (三)可見性

只有在下列情況時,一個執行緒對欄位的修改才能確保對另一個執行緒可見:

一個寫執行緒釋放一個鎖之後,另一個讀執行緒隨後獲取了同一個鎖。本質上,執行緒釋放鎖時會將強制重新整理工作記憶體中的髒資料到主記憶體中,獲取一個鎖將強制執行緒裝載(或重新裝載)欄位的值。鎖提供對一個同步方法或塊的互斥性執行,執行緒執行獲取鎖和釋放鎖時,所有對欄位的訪問的記憶體效果都是已定義的。

注意同步的雙重含義:鎖提供高階同步協議,同時線上程執行同步方法或塊時,記憶體系統(有時通過記憶體屏障指令)保證值的一致性。這說明,與順序程式設計相比較,併發程式設計與分散式程式設計更加類似。同步的第二個特性可以視為一種機制:一個執行緒在執行已同步方法時,它將傳送和/或接收其他執行緒在同步方法中對變數所做的修改。從這一點來說,使用鎖和傳送訊息僅僅是語法不同而已。


如果把一個欄位宣告為volatile型,執行緒對這個欄位寫入後,在執行後續的記憶體訪問之前,執行緒必須重新整理這個欄位且讓這個欄位對其他執行緒可見(即該欄位立即重新整理)。每次對volatile欄位的讀訪問,都要重新裝載欄位的值。

一個執行緒首次訪問一個物件的欄位,它將讀到這個欄位的初始值或被某個執行緒寫入後的值。
此外,把還未構造完成的物件的引用暴露給某個執行緒,這是一個錯誤的做法 (see ?.1.2)。在建構函式內部開始一個新執行緒也是危險的,特別是這個類可能被子類化時。Thread.start有如下的記憶體效果:呼叫start方法的執行緒釋放了鎖,隨後開始執行的新執行緒獲取了這個鎖。如果在子類建構函式執行之前,可執行的超類呼叫了new Thread(this).start(),當run方法執行時,物件很可能還沒有完全初始化。同樣,如果你建立且開始一個新執行緒T,這個執行緒使用了在執行start之後才建立的一個物件X。你不能確信X的欄位值將能對執行緒T可見。除非你把所有用到X的引用的方法都同步。如果可行的話,你可以在開始T執行緒之前建立X。

執行緒終止時,所有寫過的變數值都要重新整理到主記憶體中。比如,一個執行緒使用Thread.join來終止另一個執行緒,那麼第一個執行緒肯定能看到第二個執行緒對變數值得修改。

注意,在同一個執行緒的不同方法之間傳遞物件的引用,永遠也不會出現記憶體可見性問題。
記憶體模型確保上述操作最終會發生,一個執行緒對一個特定欄位的特定更新,最終將會對其他執行緒可見,但這個“最終”可能是很長一段時間。執行緒之間沒有同步時,很難保證對欄位的值能在多執行緒之間保持一致(指寫執行緒對欄位的寫入立即能對讀執行緒可見)。特別是,如果欄位不是volatile或沒有通過同步來訪問這個欄位,在一個迴圈中等待其他執行緒對這個欄位的寫入,這種情況總是錯誤的(see ?.2.6)。

在缺乏同步的情況下,模型還允許不一致的可見性。比如,得到一個物件的一個欄位的最新值,同時得到這個物件的其他欄位的過期的值。同樣,可能讀到一個引用變數的最新值,但讀取到這個引用變數引用的物件的欄位的過期值。
不管怎樣,執行緒之間的可見性並不總是失效(指執行緒即使沒有使用同步,仍然有可能讀取到欄位的最新值),記憶體模型僅僅是允許這種失效發生而已。因此,即使多個執行緒之間沒有使用同步,也不保證一定會發生記憶體可見性問題(指執行緒讀取到過期的值),java記憶體模型僅僅是允許記憶體可見性問題發生而已。在很多當前的JVM實現和java執行平臺中,甚至是在那些使用多處理器的JVM和平臺中,也很少出現記憶體可見性問題。共享同一個CPU的多個執行緒使用公共的快取,缺少強大的編譯器優化,以及存在強快取一致性的硬體,這些都會使執行緒更新後的值能夠立即在多執行緒之間傳遞。這使得測試基於記憶體可見性的錯誤是不切實際的,因為這樣的錯誤極難發生。或者這種錯誤僅僅在某個你沒有使用過的平臺上發生,或僅在未來的某個平臺上發生。這些類似的解釋對於多執行緒之間的記憶體可見性問題來說非常普遍。沒有同步的併發程式會出現很多問題,包括記憶體一致性問題。

原文

Visibility
Changes to fields made by one thread are guaranteed to be visible to other threads only under the following conditions:
A writing thread releases a synchronization lock and a reading thread subsequently acquires that same synchronization lock.
In essence, releasing a lock forces a flush of all writes from working memory employed by the thread, and acquiring a lock forces a (re)load of the values of accessible fields. While lock actions provide exclusion only for the operations performed within a synchronized method or block, these memory effects are defined to cover all fields used by the thread performing the action.

Note the double meaning of synchronized: it deals with locks that permit higher-level synchronization protocols, while at the same time dealing with the memory system (sometimes via low-level memory barrier machine instructions) to keep value representations in synch across threads. This reflects one way in which concurrent programming bears more similarity to distributed programming than to sequential programming. The latter sense of synchronized may be viewed as a mechanism by which a method running in one thread indicates that it is willing to send and/or receive changes to variables to and from methods running in other threads. From this point of view, using locks and passing messages might be seen merely as syntactic variants of each other.

If a field is declared as volatile, any value written to it is flushed and made visible by the writer thread before the writer thread performs any further memory operation (i.e., for the purposes at hand it is flushed immediately). Reader threads must reload the values of volatile fields upon each access.

The first time a thread accesses a field of an object, it sees either the initial value of the field or a value since written by some other thread.
Among other consequences, it is bad practice to make available the reference to an incompletely constructed object (see ?.1.2). It can also be risky to start new threads inside a constructor, especially in a class that may be subclassed. Thread.start has the same memory effects as a lock release by the thread calling start, followed by a lock acquire by the started thread. If a Runnable superclass invokes new Thread(this).start() before subclass constructors execute, then the object might not be fully initialized when the run method executes. Similarly, if you create and start a new thread T and then create an object X used by thread T, you cannot be sure that the fields of X will be visible to T unless you employ synchronization surrounding all references to object X. Or, when applicable, you can create X before starting T.

As a thread terminates, all written variables are flushed to main memory. For example, if one thread synchronizes on the termination of another thread using Thread.join, then it is guaranteed to see the effects made by that thread (see ?.3.2).
Note that visibility problems never arise when passing references to objects across methods in the same thread.
The memory model guarantees that, given the eventual occurrence of the above operations, a particular update to a particular field made by one thread will eventually be visible to another. But eventually can be an arbitrarily long time. Long stretches of code in threads that use no synchronization can be hopelessly out of synch with other threads with respect to values of fields. In particular, it is always wrong to write loops waiting for values written by other threads unless the fields are volatile or accessed via synchronization (see ?.2.6).

The model also allows inconsistent visibility in the absence of synchronization. For example, it is possible to obtain a fresh value for one field of an object, but a stale value for another. Similarly, it is possible to read a fresh, updated value of a reference variable, but a stale value of one of the fields of the object now being referenced.

However, the rules do not require visibility failures across threads, they merely allow these failures to occur. This is one aspect of the fact that not using synchronization in multithreaded code doesn’t guarantee safety violations, it just allows them. On most current JVM implementations and platforms, even those employing multiple processors, detectable visibility failures rarely occur. The use of common caches across threads sharing a CPU, the lack of aggressive compiler-based optimizations, and the presence of strong cache consistency hardware often cause values to act as if they propagate immediately among threads. This makes testing for freedom from visibility-based errors impractical, since such errors might occur extremely rarely, or only on platforms you do not have access to, or only on those that have not even been built yet. These same comments apply to multithreaded safety failures more generally. Concurrent programs that do not use synchronization fail for many reasons, including memory consistency problems.


程曉明

程曉明,Java軟體工程師,專注於併發程式設計,就職於富士通南大。個人郵箱:[email protected]