Java記憶體模型FAQ(十)volatile是幹什麼用的
Volatile欄位是用於執行緒間通訊的特殊欄位。每次讀volatile欄位都會看到其它執行緒寫入該欄位的最新值;實際上,程式設計師之所以要定義volatile欄位是因為在某些情況下由於快取和重排序所看到的陳舊的變數值是不可接受的。編譯器和執行時禁止在暫存器裡面分配它們。它們還必須保證,在它們寫好之後,它們被從緩衝區重新整理到主存中,因此,它們立即能夠對其他執行緒可見。相同地,在讀取一個volatile欄位之前,緩衝區必須失效,因為值是存在於主存中而不是本地處理器緩衝區。在重排序訪問volatile變數的時候還有其他的限制。
在舊的記憶體模型下,訪問volatile變數不能被重排序,但是,它們可能和訪問非volatile變數一起被重排序。這破壞了volatile欄位從一個執行緒到另外一個執行緒作為一個訊號條件的手段。
在新的記憶體模型下,volatile變數仍然不能彼此重排序。和舊模型不同的時候,volatile周圍的普通欄位的也不再能夠隨便的重排序了。寫入一個volatile欄位和釋放監視器有相同的記憶體影響,而且讀取volatile欄位和獲取監視器也有相同的記憶體影響。事實上,因為新的記憶體模型在重排序volatile欄位訪問上面和其他欄位(volatile或者非volatile)訪問上面有了更嚴格的約束。當執行緒A寫入一個volatile欄位f的時候,如果執行緒B讀取f的話 ,那麼對執行緒A可見的任何東西都變得對執行緒B可見了。
如下例子展示了volatile欄位應該如何使用:
class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { //uses x - guaranteed to see 42. } } }
假設一個執行緒叫做“writer”,另外一個執行緒叫做“reader”。對變數v的寫操作會等到變數x寫入到記憶體之後,然後讀執行緒就可以看見v的值。因此,如果reader執行緒看到了v的值為true,那麼,它也保證能夠看到在之前發生的寫入42這個操作。而這在舊的記憶體模型中卻未必是這樣的。如果v不是volatile變數,那麼,編譯器可以在writer執行緒中重排序寫入操作,那麼reader執行緒中的讀取x變數的操作可能會看到0。
實際上,volatile的語義已經被加強了,已經快達到同步的級別了。為了可見性的原因,每次讀取和寫入一個volatile欄位已經像一個半同步操作了
重點注意:對兩個執行緒來說,為了正確的設定happens-before關係,訪問相同的volatile變數是很重要的。以下的結論是不正確的:當執行緒A寫volatile欄位f的時候,執行緒A可見的所有東西,線上程B讀取volatile的欄位g之後,變得對執行緒B可見了。釋放操作和獲取操作必須匹配(也就是在同一個volatile欄位上面完成)。
原文
What does volatile do?
Volatile fields are special fields which are used for communicating state between threads. Each read of a volatile will see the last write to that volatile by any thread; in effect, they are designated by the programmer as fields for which it is never acceptable to see a “stale” value as a result of caching or reordering. The compiler and runtime are prohibited from allocating them in registers. They must also ensure that after they are written, they are flushed out of the cache to main memory, so they can immediately become visible to other threads. Similarly, before a volatile field is read, the cache must be invalidated so that the value in main memory, not the local processor cache, is the one seen. There are also additional restrictions on reordering accesses to volatile variables.
Under the old memory model, accesses to volatile variables could not be reordered with each other, but they could be reordered with nonvolatile variable accesses. This undermined the usefulness of volatile fields as a means of signaling conditions from one thread to another.
Under the new memory model, it is still true that volatile variables cannot be reordered with each other. The difference is that it is now no longer so easy to reorder normal field accesses around them. Writing to a volatile field has the same memory effect as a monitor release, and reading from a volatile field has the same memory effect as a monitor acquire. In effect, because the new memory model places stricter constraints on reordering of volatile field accesses with other field accesses, volatile or not, anything that was visible to thread A when it writes to volatile field f becomes visible to thread B when it reads f.
Here is a simple example of how volatile fields can be used:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}
Assume that one thread is calling writer, and another is calling reader. The write to v in writer releases the write to x to memory, and the read of v acquires that value from memory. Thus, if the reader sees the value true for v, it is also guaranteed to see the write to 42 that happened before it. This would not have been true under the old memory model. If v were not volatile, then the compiler could reorder the writes in writer, and reader‘s read of x might see 0.
Effectively, the semantics of volatile have been strengthened substantially, almost to the level of synchronization. Each read or write of a volatile field acts like “half” a synchronization, for purposes of visibility.
Important Note: Note that it is important for both threads to access the same volatile variable in order to properly set up the happens-before relationship. It is not the case that everything visible to thread A when it writes volatile field f becomes visible to thread B after it reads volatile field g. The release and acquire have to “match” (i.e., be performed on the same volatile field) to have the right semantics.