Java記憶體模型FAQ(一) 什麼是記憶體模型

喝水會長肉發表於2021-12-02

在多核系統中,處理器一般有一層或者多層的快取,這些的快取通過加速資料訪問(因為資料距離處理器更近)和降低共享記憶體在匯流排上的通訊(因為本地快取能夠滿足許多記憶體操作)來提高CPU效能。快取能夠大大提升效能,但是它們也帶來了許多挑戰。例如,當兩個CPU同時檢查相同的記憶體地址時會發生什麼?在什麼樣的條件下它們會看到相同的值?


在處理器層面上,記憶體模型定義了一個充要條件,“讓當前的處理器可以看到其他處理器寫入到記憶體的資料”以及“其他處理器可以看到當前處理器寫入到記憶體的資料”。有些處理器有很強的記憶體模型(strong memory model),能夠讓所有的處理器在任何時候任何指定的記憶體地址上都可以看到完全相同的值。而另外一些處理器則有較弱的記憶體模型(weaker memory model),在這種處理器中,必須使用記憶體屏障(一種特殊的指令)來重新整理本地處理器快取並使本地處理器快取無效,目的是為了讓當前處理器能夠看到其他處理器的寫操作或者讓其他處理器能看到當前處理器的寫操作。這些記憶體屏障通常在lock和unlock操作的時候完成。記憶體屏障在高階語言中對程式設計師是不可見的。

在強記憶體模型下,有時候編寫程式可能會更容易,因為減少了對記憶體屏障的依賴。但是即使在一些最強的記憶體模型下,記憶體屏障仍然是必須的。設定記憶體屏障往往與我們的直覺並不一致。近來處理器設計的趨勢更傾向於弱的記憶體模型,因為弱記憶體模型削弱了快取一致性,所以在多處理器平臺和更大容量的記憶體下可以實現更好的可伸縮性

“一個執行緒的寫操作對其他執行緒可見”這個問題是因為編譯器對程式碼進行重排序導致的。例如,只要程式碼移動不會改變程式的語義,當編譯器認為程式中移動一個寫操作到後面會更有效的時候,編譯器就會對程式碼進行移動。如果編譯器推遲執行一個操作,其他執行緒可能在這個操作執行完之前都不會看到該操作的結果,這反映了快取的影響。

此外,寫入記憶體的操作能夠被移動到程式裡更前的時候。在這種情況下,其他的執行緒在程式中可能看到一個比它實際發生更早的寫操作。所有的這些靈活性的設計是為了通過給編譯器,執行時或硬體靈活性使其能在最佳順序的情況下來執行操作。在記憶體模型的限定之內,我們能夠獲取到更高的效能。

看下面程式碼展示的一個簡單例子:

ClassReordering 
{


int x = 0 , y = 0 ;

public void writer ( ) {

x = 1 ;

y = 2 ;

}

public void reader ( ) {

int r1 = y ;

int r2 = x ;

  }

}

讓我們看在兩個併發執行緒中執行這段程式碼,讀取Y變數將會得到2這個值。因為這個寫入比寫到X變數更晚一些,程式設計師可能認為讀取X變數將肯定會得到1。但是,寫入操作可能被重排序過。如果重排序發生了,那麼,就能發生對Y變數的寫入操作,讀取兩個變數的操作緊隨其後,而且寫入到X這個操作能發生。程式的結果可能是r1變數的值是2,但是r2變數的值為0。

Java記憶體模型描述了在多執行緒程式碼中哪些行為是合法的,以及執行緒如何通過記憶體進行互動。它描述了“程式中的變數“ 和 ”從記憶體或者暫存器獲取或儲存它們的底層細節”之間的關係。Java記憶體模型通過使用各種各樣的硬體和編譯器的優化來正確實現以上事情。

Java包含了幾個語言級別的關鍵字,包括:volatile, final以及synchronized,目的是為了幫助程式設計師向編譯器描述一個程式的併發需求。Java記憶體模型定義了volatile和synchronized的行為,更重要的是保證了同步的java程式在所有的處理器架構下面都能正確的執行。

原文

What is a memory model, anyway?

In multiprocessor systems, processors generally have one or more layers of memory cache, which improves performance both by speeding access to data (because the data is closer to the processor) and reducing traffic on the shared memory bus (because many memory operations can be satisfied by local caches.) Memory caches can improve performance tremendously, but they present a host of new challenges. What, for example, happens when two processors examine the same memory location at the same time? Under what conditions will they see the same value?

At the processor level, a memory model defines necessary and sufficient conditions for knowing that writes to memory by other processors are visible to the current processor, and writes by the current processor are visible to other processors. Some processors exhibit a strong memory model, where all processors see exactly the same value for any given memory location at all times. Other processors exhibit a weaker memory model, where special instructions, called memory barriers, are required to flush or invalidate the local processor cache in order to see writes made by other processors or make writes by this processor visible to others. These memory barriers are usually performed when lock and unlock actions are taken; they are invisible to programmers in a high level language.

It can sometimes be easier to write programs for strong memory models, because of the reduced need for memory barriers. However, even on some of the strongest memory models, memory barriers are often necessary; quite frequently their placement is counterintuitive. Recent trends in processor design have encouraged weaker memory models, because the relaxations they make for cache consistency allow for greater scalability across multiple processors and larger amounts of memory.

The issue of when a write becomes visible to another thread is compounded by the compiler’s reordering of code. For example, the compiler might decide that it is more efficient to move a write operation later in the program; as long as this code motion does not change the program’s semantics, it is free to do so.  If a compiler defers an operation, another thread will not see it until it is performed; this mirrors the effect of caching.

Moreover, writes to memory can be moved earlier in a program; in this case, other threads might see a write before it actually “occurs” in the program.  All of this flexibility is by design — by giving the compiler, runtime, or hardware the flexibility to execute operations in the optimal order, within the bounds of the memory model, we can achieve higher performance.

A simple example of this can be seen in the following code:

ClassReordering 
{

//java學習交流:737251827  進入可領取學習資源及對十年開發經驗大佬提問,免費解答!
int x = 0 , y = 0 ;

public void writer ( ) {

x = 1 ;

y = 2 ;

}

public void reader ( ) {

int r1 = y ;

int r2 = x ;

  }

}


Let’s say that this code is executed in two threads concurrently, and the read of y sees the value 2. Because this write came after the write to x, the programmer might assume that the read of x must see the value 1. However, the writes may have been reordered. If this takes place, then the write to y could happen, the reads of both variables could follow, and then the write to x could take place. The result would be that r1 has the value 2, but r2 has the value 0.

The Java Memory Model describes what behaviors are legal in multithreaded code, and how threads may interact through memory. It describes the relationship between variables in a program and the low-level details of storing and retrieving them to and from memory or registers in a real computer system. It does this in a way that can be implemented correctly using a wide variety of hardware and a wide variety of compiler optimizations.

Java includes several language constructs, including volatile, final, and synchronized, which are intended to help the programmer describe a program’s concurrency requirements to the compiler. The Java Memory Model defines the behavior of volatile and synchronized, and, more importantly, ensures that a correctly synchronized Java program runs correctly on all processor architectures.



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70010294/viewspace-2845370/,如需轉載,請註明出處,否則將追究法律責任。

相關文章