1. 程式人生 > >Java記憶體模型FAQ(七)同步會幹些什麼呢

Java記憶體模型FAQ(七)同步會幹些什麼呢

譯者:Alex

同步有幾個方面的作用。最廣為人知的就是互斥 ——一次只有一個執行緒能夠獲得一個監視器,因此,在一個監視器上面同步意味著一旦一個執行緒進入到監視器保護的同步塊中,其他的執行緒都不能進入到同一個監視器保護的塊中間,除非第一個執行緒退出了同步塊。

但是同步的含義比互斥更廣。同步保證了一個執行緒在同步塊之前或者在同步塊中的一個記憶體寫入操作以可預知的方式對其他有相同監視器的執行緒可見。當我們退出了同步塊,我們就釋放了這個監視器,這個監視器有重新整理緩衝區到主記憶體的效果,因此該執行緒的寫入操作能夠為其他執行緒所見。在我們進入一個同步塊之前,我們需要獲取監視器,監視器有使本地處理器快取失效的功能,因此變數會從主存重新載入,於是其它執行緒對共享變數的修改對當前執行緒來說就變得可見了。

依據快取來討論同步,可能聽起來這些觀點僅僅會影響到多處理器的系統。但是,重排序效果能夠在單一處理器上面很容易見到。對編譯器來說,在獲取之前或者釋放之後移動你的程式碼是不可能的。當我們談到在緩衝區上面進行的獲取和釋放操作,我們使用了簡述的方式來描述大量可能的影響。

新的記憶體模型語義在記憶體操作(讀取欄位,寫入欄位,鎖,解鎖)以及其他執行緒的操作(start 和 join)中建立了一個部分排序,在這些操作中,一些操作被稱為happen before其他操作。當一個操作在另外一個操作之前發生,第一個操作保證能夠排到前面並且對第二個操作可見。這些排序的規則如下:

  • 執行緒中的每個操作happens before
    該執行緒中在程式順序上後續的每個操作。
  • 解鎖一個監視器的操作happens before隨後對相同監視器進行鎖的操作。
  • 對volatile欄位的寫操作happens before後續對相同volatile欄位的讀取操作。
  • 執行緒上呼叫start()方法happens before這個執行緒啟動後的任何操作。
  • 一個執行緒中所有的操作都happens before從這個執行緒join()方法成功返回的任何其他執行緒。(注意思是其他執行緒等待一個執行緒的jion()方法完成,那麼,這個執行緒中的所有操作happens before其他執行緒中的所有操作)

這意味著:任何記憶體操作,這個記憶體操作在退出一個同步塊前對一個執行緒是可見的,對任何執行緒在它進入一個被相同的監視器保護的同步塊後都是可見的,因為所有記憶體操作happens before釋放監視器以及釋放監視器happens before獲取監視器。

其他如下模式的實現被一些人用來強迫實現一個記憶體屏障的,不會生效:

synchronized (new Object()) {}

這段程式碼其實不會執行任何操作,你的編譯器會把它完全移除掉,因為編譯器知道沒有其他的執行緒會使用相同的監視器進行同步。要看到其他執行緒的結果,你必須為一個執行緒建立happens before關係。

重點注意:對兩個執行緒來說,為了正確建立happens before關係而在相同監視器上面進行同步是非常重要的。以下觀點是錯誤的:當執行緒A在物件X上面同步的時候,所有東西對執行緒A可見,執行緒B在物件Y上面進行同步的時候,所有東西對執行緒B也是可見的。釋放監視器和獲取監視器必須匹配(也就是說要在相同的監視器上面完成這兩個操作),否則,程式碼就會存在“資料競爭”。

原文

What does synchronization do?

Synchronization has several aspects. The most well-understood is mutual exclusion — only one thread can hold a monitor at once, so synchronizing on a monitor means that once one thread enters a synchronized block protected by a monitor, no other thread can enter a block protected by that monitor until the first thread exits the synchronized block.

But there is more to synchronization than mutual exclusion. Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor. After we exit a synchronized block, we release the monitor, which has the effect of flushing the cache to main memory, so that writes made by this thread can be visible to other threads. Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory. We will then be able to see all of the writes made visible by the previous release.

Discussing this in terms of caches, it may sound as if these issues only affect multiprocessor machines. However, the reordering effects can be easily seen on a single processor. It is not possible, for example, for the compiler to move your code before an acquire or after a release. When we say that acquires and releases act on caches, we are using shorthand for a number of possible effects.

The new memory model semantics create a partial ordering on memory operations (read field, write field, lock, unlock) and other thread operations (start and join), where some actions are said to happen before other operations. When one action happens before another, the first is guaranteed to be ordered before and visible to the second. The rules of this ordering are as follows:

  • 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.
  • A write to a volatile field happens before every subsequent read of that same volatile.
  • A call to start() on a thread happens before any actions in the started thread.
  • All actions in a thread happen before any other thread successfully returns from a join() on that thread.

This means that any memory operations which were visible to a thread before exiting a synchronized block are visible to any thread after it enters a synchronized block protected by the same monitor, since all the memory operations happen before the release, and the release happens before the acquire.

Another implication is that the following pattern, which some people use to force a memory barrier, doesn’t work:

synchronized (new Object()) {}

This is actually a no-op, and your compiler can remove it entirely, because the compiler knows that no other thread will synchronize on the same monitor. You have to set up a happens-before relationship for one thread to see the results of another.

Important Note: Note that it is important for both threads to synchronize on the same monitor in order to set up the happens-before relationship properly. It is not the case that everything visible to thread A when it synchronizes on object X becomes visible to thread B after it synchronizes on object Y. The release and acquire have to “match” (i.e., be performed on the same monitor) to have the right semantics. Otherwise, the code has a data race.