1. 程式人生 > >Java記憶體模型與執行緒

Java記憶體模型與執行緒

Java記憶體模型

Java虛擬機器在規範中檢視定義一種Java記憶體模型(Java Memory Model,JMM)來遮蔽掉各種硬體和作業系統的訪問差異,以實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果。

主記憶體與工作記憶體

Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,此處的變數(Variables)包括例項欄位、靜態欄位和構成陣列物件的元素,但不包括區域性變數和方法引數,因為後者是執行緒私有的。
Java記憶體模型規定了所有的變數都儲存在主記憶體(Main Memory)中。每條執行緒都擁有自己的工作記憶體(Working Memory),執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作(讀取、賦值等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需通過主記憶體來完成,執行緒、主記憶體、工作記憶體三者的互動關係如下圖所示:
這裡寫圖片描述

記憶體間互動操作

關於一個變數如何如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之類的實現細節,Java記憶體模型中定義了以下8種操作來完成,虛擬機器實現時必須保證下面提及的每一種操作都是原子的、不可再分的。

  • lock(鎖定):作用於主記憶體變數,它把一個變數標識為一條執行緒獨佔的狀態。
  • unlock(解鎖):作用於主記憶體變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其它執行緒鎖定。
  • read(讀取):作用於主記憶體變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。
  • load(載入):作用於工作記憶體變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
  • use(使用):作用於工作記憶體變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
  • assign(賦值):作用於工作記憶體變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  • store(儲存):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便write操作使用。
  • write(寫入):作用於主記憶體變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。

如果要把一個變數從主記憶體複製到工作記憶體,那就要順序地執行read和load操作,如果要把變數從工作記憶體同步回主記憶體,就要順序地執行store和write操作。這兩組操作都只需要順序執行而不必連續。除此之外,Java記憶體模型還規定了在執行上述8種基本操作時必須滿足如下規則:

  • 不允許你read和load、store和write操作之一單獨出現,即不允許一個變數從主記憶體讀取了但工作記憶體不接受,或者從工作記憶體發起回寫了但主記憶體不接受的情況出現。
  • 不允許一個執行緒丟棄它的assign操作,即變數在工作記憶體中改變了之後必須把該變化同步回主記憶體。
  • 不允許一個執行緒無原因地(沒有發生過任何assign)把資料從執行緒的工作記憶體同步到主記憶體。
  • 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數,換句話說,就是對一個變數實施use、store之前,必須先執行過了assign和load操作。
  • 一個變數在同一時刻只允許一條執行緒對其進行lock操作,單lock操作可以被同一條執行緒重複執行多次,多次執行lock之後,只有執行相同次數的unlock操作,變數才會被解鎖。
  • 如果對一個變數執行lock操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值。
  • 如果一個變數事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其它執行緒鎖定的變數。
  • 對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中(只是sotore、write操作)。

volatile關鍵字

關鍵字volatile是Java虛擬機器提供的最輕量級的同步機制,瞭解volatile變數的語義對後面瞭解多執行緒操作的其它特性很有意義。首先,我們來了解一下這個關鍵字的作用。
當一個變數定義為volatile之後,它將具備兩種屬性,第一種是保證此變數對所有執行緒的可見性,這裡的“可見性”是指當一條執行緒修改了這個變數的值,新值對於其它執行緒來說是可以立即得知的。不過,因為Java裡面的運算並不是原子操作,所以volatile變數的運算在併發條件下並不是絕對安全的。以下為示例程式碼:

package com.overridere.twelve;
/**
 * volatile變數自增運算測試
 */
public class VolatileTest {

    public static volatile int race = 0;

    public static void increase() {
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        // 等待所有累加執行緒都結束
        while (Thread.activeCount() > 1)
            Thread.yield();

        System.out.println(race);
    }
}

這段程式碼發起了20個執行緒,每個執行緒都對變數race進行10000次自增操作,如果race是執行緒安全的,最後輸出結果應該是200000,但真實結果都是一個小於200000的值,這是問為什麼呢?

 public static void increase();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #13                 // Field race:I
         3: iconst_1
         4: iadd
         5: putstatic     #13                 // Field race:I
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

問題就在“race++”中,用javap命令得到上面的位元組碼命令,發現只有一行程式碼的increase()方法在Class檔案中是由4條位元組碼指令構成的,從這上面分析就很容易分析出執行緒不安全的原因了:當getstatic指令把race的值取到運算元棧頂的時候,volatile關鍵字保證了race的值在此時是正確的,但是在執行iconst_1、iadd這些指令的時候,其它執行緒可能已經把race加大了,而操作棧頂的值就變成了過期的資料。意思是說,在這個例子當中,volatile關鍵字只保證getstatic指令從常量池中將值取出放到運算元棧頂這個動作是執行緒安全的(從主記憶體到工作記憶體),作用範圍就是這個,之後就會失去作用,後面的增加操作就不是執行緒安全的了。

  • 運算結果並不依賴變數的當前值,或者能夠保證只有單一的執行緒修改變數的值。
  • 變數不需要與其它的變數共同參與不變約束。

第一句話的意思是:不管我以前是什麼樣的,都不影響現在的我。
第二句話的意思是:不管別人是什麼樣的,只要跟我沒關係,就不會影響到我。
如下面的示例程式碼:

volatile boolean isVolatile;
public void shutdown(){
    isVolatile = true;
}
public void doWork(){
    while(!isVolatile){
        //do stuff
    }
}

上面程式碼中的isVolatile變數,無論之前是什麼,都不妨礙當前執行緒賦值,也沒有與其它變數綁起來共同參與不變約束。

使用volatile變數的第二個語義是禁止指令重排序優化,普通的變數僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變數賦值操作的順序與程式程式碼中的執行順序一致。因為在一個執行緒的方法執行過程中無法感知到這點,也就是Java記憶體模型中描述的所謂“執行緒內表現為序列的語義”(Within-Thread As-If-Serial Semantics)。

指令重排序意思就是不按程式規定的順序執行指令,但並不是說指令任意重排。比如說,指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值減去3,這時指令1和指令2是有依賴的,它們之間的順序不能重排——(A+10)*2與A*2+10顯然不相等,但是指令3可以重排到指令1、2之前或者中間。

而valotile關鍵字是如何實現禁止指令重排序呢?
有volatile關鍵字修飾的變數,賦值後會多執行一個操作,這個操作會把修改同步到記憶體,意味著所有之前的操作都執行完畢了,這個操作相當於記憶體屏障(Memory Barrier),這樣一來指令重排序的時候就不能把後面的指令重排序到記憶體屏障之前的位置。只有一個CPU訪問記憶體時,不需要記憶體屏障。

原子性、可見性與有序性

原子性(Atomicity):由Java記憶體模型來直接保證的原子性變數操作包括read、load、assign、use、和write。
可見性(Visibility):可見性是指當一個執行緒修改了共享變數的值,其它執行緒能夠立即得知這個修改。Java記憶體模型是通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方式來實現可見性的。volatile變數與普通變數的區別是,volatile的特殊規則保證新值能立即同步到主記憶體,每次使用前立即從主記憶體重新整理。
有序性(Ordering):如果在本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有的操作都是無序的。前半句是指“執行緒內表現為序列的語義”,後半句是指“指令重排序”現象和“工作記憶體和主記憶體同步延遲”現象。

現行發生原則

先行發生是Java記憶體模型中定義的兩項操作之間的偏序關係,如果說操作A先行發生於操作B,意思就是在發生操作B之前,操作A產生的影響能被操作B觀察到。請看下面的例子:

//以下操作線上程A中執行

i = 1;
//以下操作線上程B中執行
j = i;

//以下操作線上程C中執行
i = 2;

假設執行緒A中的操作先行發生於執行緒B的操作,name可以確定線上程B的操作執行之後,變數j的值一定等於i,依據有兩個:以是根據現行發生原則,“i=1”的結果可以被執行緒B觀察到;二是執行緒C還沒執行,執行緒A操作結束之後沒有其它執行緒會修改變數i的值。
再把問題變一下,我們依然保持執行緒A和執行緒B的先行發生關係,而執行緒C出現線上程A和執行緒B之間,但是執行緒C和執行緒B沒有先行發生關係,那麼j的值會是多少呢?答案是不確定,因為執行緒C和執行緒B不確定哪條執行緒先執行到對變數j的修改。

相關推薦

十一、JVM(HotSpot)Java記憶體模型執行

注:本博文主要是基於JDK1.7會適當加入1.8內容。 1、Java記憶體模型 記憶體模型:在特定的操作協議下,對特定的記憶體或快取記憶體進行讀寫訪問的抽象過程。不同的物理機擁有不一樣的記憶體模型,而Java虛擬機器也擁有自己的記憶體模型。 主要目標:定義程式中各個變數的訪問規則,

Java記憶體模型執行知識點總結

首先討論一下物理機對於併發的處理方案 運算任務不可能只靠處理器簡單的計算就能完成,必須還要增加與記憶體的互動操作(如讀取資料,儲存資料), 由於計算機的儲存裝置與處理器的運算速度之間有著幾個數量級的差距,所以現代計算機系統選擇加入快取記憶體(Cache)來進行記憶體與處理器之間的快取來提高效率 由於快取記

Java記憶體模型執行——Java記憶體模型

文章目錄 一、主記憶體與工作記憶體 1.1 Java記憶體模型中的變數 1.2 主記憶體與工作記憶體 二、主記憶體與工作記憶體間互動操作 三、對於volatile型變數的特殊規則 3.1 可見性 3.2

Java記憶體模型執行——硬體的效率一致性,記憶體模型

文章目錄 一、先來一個問題,想要電腦快,買記憶體條還是固態硬碟? 二、衡量一個服務效能好壞的標準之一 三、硬體的效率與一致性 3.1 硬體的效率與一致性問題是怎樣出來的? 四、記憶體模型 一、先來一個問題

Java記憶體模型執行 深入理解Java虛擬機器總結

在許多情況下,讓計算機同時去做幾件事情,不僅是因為計算機的運算能力強大了,還有一個很重要的原因是計算機的運算速度與它的儲存和通訊子系統速度的差距太大, 大量的時間都花費在磁碟I/O、網路通訊或者資料庫訪問上。 如果不希望處理器在大部分時間裡都處於等待其他資源的狀態,就必須使用一些手段去把處理器

1.java一切即物件以及java記憶體模型執行

由此可以得知: 程式碼完成之後進行本地配置的一些讀取操作: 至此可以得知其編譯模式是mixed模式的 new date()預設輸出的結果是import中包的預設建構函式初始化後的結果: 觀看Date類原始碼即可得知: 鑑於java是單繼承關係,由此來看一下imp

讀書筆記 ---- 《深入理解Java虛擬機器》---- 第11篇:Java記憶體模型執行

上一篇:晚期(執行期)優化:https://blog.csdn.net/pcwl1206/article/details/84642835 目  錄: 1  概述 2  Java記憶體模型 2.1  主記憶體與工作記憶體 2.2 

Java虛擬機器】Java記憶體模型執行

Java記憶體模型與執行緒 Java記憶體模型 記憶體間互動操作 volatile關鍵字 Java與執行緒 核心實現 使用使用者執行緒實現 使用使用者執行緒加輕量級程序混合實現 Java執行緒的實現

深入理解JVM(十一)——Java記憶體模型執行

計算機運算的速度,與它的儲存和通訊子系統相差太大,大量的時間花費在磁碟IO,網路通訊和資料庫上。 衡量一個服務效能的高低好壞,每秒事務處理數TPS是最重要的指標。 對於計算量相同的任務,程式執行緒併發協調的越有條不紊,效率越高;反之,執行緒之間頻繁阻塞或是死鎖,將大大降低併發能力。

深入理解 Java 虛擬機器(十二)Java 記憶體模型執行

執行緒安全 Java 語言中的執行緒安全 根據執行緒安全的強度排序,Java 語言中各種操作共享的資料可以分為 5 類:不可變、絕對執行緒安全、相對執行緒安全、執行緒相容、執行緒對立。 不可變 不可變的物件一定是執行緒安全的,如果共享資料是一個基本資料型別,那麼

java記憶體模型執行(1)

一、處理器、快取記憶體、主記憶體之前的互動圖 二、Java記憶體模型 倆張圖之間的關係很清晰 一個處理器對應一個執行緒 一個快取記憶體對應一個工作記憶體 問題的關鍵點就在於:java執行緒之間與工作記憶體打交道,而不是主記憶體,工作記憶體之間沒有直接的關

java記憶體模型執行(2)

一、原子性、可見性與有序性 1.原子性 原子性操作包括read、load、asign、use、store和write 更大範圍的原子性保證:lock和unlock(倆者未開放),monitorenter和monitorexit(隱式的使用synchronized)

深入理解Java虛擬機器讀書筆記8----Java記憶體模型執行

八 Java記憶體模型與執行緒   1 Java記憶體模型     ---主要目標:定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。     ---此處的變數和Java中的變

jdk原始碼解析(十一)——Java記憶體模型執行

前面我們瞭解了Java的編譯和執行,這裡在講解一下高效併發(Java記憶體模型與執行緒)在瞭解記憶體模型與執行緒之前,我們先要了解一些東西。 1 硬體效率與一致性  計算併發執行的執行和充分利用計算機處理器的效能兩者看來是互為因果的,而在大多數的時候,計算機的處理速度不止是在處理器

理解JVM(五):Java記憶體模型執行

Java記憶體模型 JMM(Java Memory Model)是JVM定義的記憶體模型,用來遮蔽各種硬體和作業系統的記憶體訪問差異。 * 主記憶體:所有的變數都儲存在主記憶體(Main Memory,類比實體記憶體)中。 * 工作記憶體:每條執行緒有自己

深入理解java虛擬機器----java記憶體模型執行

12.1  概述 衡量一個服務效能的高低好壞,每秒事務處理數是最重要的指標之一,它 著一秒內服務端平均能響應的請求總數,而TPS的與程式的併發能力又有非常密切的關係。 12.2 硬體的效率與一致性 因為有快取一致性,所以要有一些操作來保證安全。 12.3 java記憶

Java記憶體模型執行

Java記憶體模型 Java虛擬機器在規範中檢視定義一種Java記憶體模型(Java Memory Model,JMM)來遮蔽掉各種硬體和作業系統的訪問差異,以實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果。 主記憶體與工作記憶體 Java記憶

深入理解Java虛擬機器JVM高階特性最佳實踐閱讀總結—— 第十二章 Java記憶體模型執行

Java記憶體模型JMM,主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體讀取變數的底層細節,這裡的變數不包括執行緒私有的變數,如區域性引數;記憶體模型規定所有變數儲存在主記憶體;每個執行緒都有自己的工作記憶體,其中儲存了該執行緒用到的變數

Java虛擬機器——Java記憶體模型執行 [待更新]

12.2硬體的效率與一致性 處理器與記憶體速度矛盾--> 1.引入快取記憶體-->新的問題:快取一致性(Cache Coherence) 2.指令重排優化( Instruc

《深入理解Java虛擬機器》-----第12章 Java記憶體模型執行

概述 多工處理在現代計算機作業系統中幾乎已是一項必備的功能了。在許多情況下,讓計算機同時去做幾件事情,不僅是因為計算機的運算能力強大了,還有一個很重要的原因是計算機的運算速度與它的儲存和通訊子系統速度的差距太大,大量的時間都花費在磁碟I/O、網路通訊或者資料庫訪問上。如果不希望處理器在大部分時間裡都處於等待