1. 程式人生 > >[高併發Java 三] Java記憶體模型和執行緒安全

[高併發Java 三] Java記憶體模型和執行緒安全

網上很多資料在描述Java記憶體模型的時候,都會介紹有一個主存,然後每個工作執行緒有自己的工作記憶體。資料在主存中會有一份,在工作記憶體中也有一份。工作記憶體和主存之間會有各種原子操作去進行同步。

但是由於Java版本的不斷演變,記憶體模型也進行了改變。本文只講述Java記憶體模型的一些特性,無論是新的記憶體模型還是舊的記憶體模型,在明白了這些特性以後,看起來也會更加清晰。

1. 原子性

  • 原子性是指一個操作是不可中斷的。即使是在多個執行緒一起執行的時候,一個操作一旦開始,就不會被其它執行緒干擾。

一般認為cpu的指令都是原子操作,但是我們寫的程式碼就不一定是原子操作了。

比如說i++。這個操作不是原子操作,基本分為3個操作,讀取i,進行+1,賦值給i。

假設有兩個執行緒,當第一個執行緒讀取i=1時,還沒進行+1操作,切換到第二個執行緒,此時第二個執行緒也讀取的是i=1。隨後兩個執行緒進行後續+1操作,再賦值回去以後,i不是3,而是2。顯然資料出現了不一致性。

再比如在32位的JVM上面去讀取64位的long型數值,也不是一個原子操作。當然32位JVM讀取32位整數是一個原子操作。

2. 有序性

  • 在併發時,程式的執行可能就會出現亂序。

計算機在執行程式碼時,不一定會按照程式的順序來執行。

classOrderExample{ 
		int a = 0; 
		boolean flag = false; 
		publicvoidwriter()
{ a = 1; flag = true; } publicvoidreader(){ if (flag) { int i = a +1; } } }
比如上述程式碼,兩個方法分別被兩個執行緒呼叫。按照常理,寫執行緒應該先執行a=1,再執行flag=true。當讀執行緒進行讀的時候,i=2;

但是因為a=1和flag=true,並沒有邏輯上的關聯。所以有可能執行的順序顛倒,有可能先執行flag=true,再執行a=1。這時當flag=true時,切換到讀執行緒,此時a=1還沒有執行,那麼讀執行緒將i=1。

當然這個不是絕對的。是有可能會發生亂序,有可能不發生。

那麼為什麼會發生亂序呢?這個要從cpu指令說起,Java中的程式碼被編譯以後,最後也是轉換成彙編碼的。

一條指令的執行是可以分為很多步驟的,假設cpu指令分為以下幾步

  • 取指 IF
  • 譯碼和取暫存器運算元 ID
  • 執行或者有效地址計算 EX
  • 儲存器訪問 MEM
  • 寫回 WB
假設這裡有兩條指令

一般來說我們會認為指令是序列執行的,先執行指令1,然後再執行指令2。假設每個步驟需要消耗1個cpu時間週期,那麼執行這兩個指令需要消耗10個cpu時間週期,這樣做效率太低。事實上指令都是並行執行的,當然在第一條指令在執行IF的時候,第二條指令是不能進行IF的,因為指令暫存器等不能被同時佔用。所以就如上圖所示,兩條指令是一種相對錯開的方式並行執行。當指令1執行ID的時候,指令2執行IF。這樣只用6個cpu時間週期就執行了兩個指令,效率比較高。

按照這個思路我們來看下A=B+C的指令是如何執行的。

如圖所示,ADD操作時有一個空閒(X)操作,因為當想讓B和C相加的時候,在圖中ADD的X操作時,C還沒從記憶體中讀取(當MEM操作完成時,C才從記憶體中讀取。這裡會有一個疑問,此時還沒有回寫(WB)到R2中,怎麼會將R1與R1相加。那是因為在硬體電路當中,會使用一種叫“旁路”的技術直接把資料從硬體當中讀取出來,所以不需要等待WB執行完才進行ADD)。所以ADD操作中會有一個空閒(X)時間。在SW操作中,因為EX指令不能和ADD的EX指令同時進行,所以也會有一個空閒(X)時間。

接下來舉個稍微複雜點的例子

a=b+c 
d=e-f

對應的指令如下圖

原因和上面的類似,這裡就不分析了。我們發現,這裡的X很多,浪費的時間週期很多,效能也被影響。有沒有辦法使X的數量減少呢?

我們希望用一些操作把X的空閒時間填充掉,因為ADD與上面的指令有資料依賴,我們希望用一些沒有資料依賴的指令去填充掉這些因為資料依賴而產生的空閒時間。

我們將指令的順序進行了改變

改變了指令順序以後,X被消除了。總體的執行時間週期也減少了。

指令重排可以使流水線更加順暢

當然指令重排的原則是不能破壞序列程式的語義,例如a=1,b=a+1,這種指令就不會重排了,因為重排的序列結果和原先的不同。

指令重排只是編譯器或者CPU的優化一種方式,而這種優化就造成了本章一開始程式的問題。

如何解決呢?用volatile關鍵字,這個後面的系列會介紹到。

3. 可見性

  • 可見性是指當一個執行緒修改了某一個共享變數的值,其他執行緒是否能夠立即知道這個修改。

可見性問題可能有各個環節產生。比如剛剛說的指令重排也會產生可見性問題,另外在編譯器的優化或者某些硬體的優化都會產生可見性問題。

比如某個執行緒將一個共享值優化到了記憶體中,而另一個執行緒將這個共享值優化到了快取中,當修改記憶體中值的時候,快取中的值是不知道這個修改的。

比如有些硬體優化,程式在對同一個地址進行多次寫時,它會認為是沒有必要的,只保留最後一次寫,那麼之前寫的資料在其他執行緒中就不可見了。

總之,可見性的問題大多都源於優化。

接下來看一個Java虛擬機器層面產生的可見性問題

問題來自於一個Blog

package edu.hushi.jvm;
 
/**
 *
 * @author -10
 *
 */
public classVisibilityTestextendsThread{
 
    private boolean stop;
 
    publicvoidrun(){
        int i = 0;
        while(!stop) {
            i++;
        }
        System.out.println("finish loop,i=" + i);
    }
 
    publicvoidstopIt(){
        stop = true;
    }
 
    publicbooleangetStop(){
        return stop;
    }
    publicstaticvoidmain(String[] args)throws Exception {
        VisibilityTest v = new VisibilityTest();
        v.start();
 
        Thread.sleep(1000);
        v.stopIt();
        Thread.sleep(2000);
        System.out.println("finish main");
        System.out.println(v.getStop());
    }
 
}
程式碼很簡單,v執行緒一直不斷的在while迴圈中i++,直到主執行緒呼叫stop方法,改變了v執行緒中的stop變數的值使迴圈停止。

看似簡單的程式碼執行時就會出現問題。這個程式在 client 模式下是能停止執行緒做自增操作的,但是在 server 模式先將是無限迴圈。(server模式下JVM優化更多)

64位的系統上面大多都是server模式,在server模式下執行:

finish main
true
只會打印出這兩句話,而不會打印出finish loop。可是能夠發現stop的值已經是true了。

該Blog作者用工具將程式還原為彙編程式碼

這裡只截取了一部分彙編程式碼,紅色部分為迴圈部分,可以清楚得看到只有在0x0193bf9d才進行了stop的驗證,而紅色部分並沒有取stop的值,所以才進行了無限迴圈。

這是JVM優化後的結果。如何避免呢?和指令重排一樣,用volatile關鍵字。

如果加入了volatile,再還原為彙編程式碼就會發現,每次迴圈都會get一下stop的值。

接下來看一些在“Java語言規範”中的示例


上圖說明了指令重排將會導致結果不同。


上圖使r5=r2的原因是,r2=r1.x,r5=r1.x,在編譯時直接將其優化成r5=r2。最後導致結果不同。

4. Happen-Before

  • 程式順序原則:一個執行緒內保證語義的序列性
  • volatile規則:volatile變數的寫,先發生於讀,這保證了volatile變數的可見性
  • 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
  • 傳遞性:A先於B,B先於C,那麼A必然先於C
  • 執行緒的start()方法先於它的每一個動作
  • 執行緒的所有操作先於執行緒的終結(Thread.join())
  • 執行緒的中斷(interrupt())先於被中斷執行緒的程式碼
  • 物件的建構函式執行結束先於finalize()方法
這些原則保證了重排的語義是一致的。

5. 執行緒安全的概念

指某個函式、函式庫在多執行緒環境中被呼叫時,能夠正確地處理各個執行緒的區域性變數,使程式功能正確完成。

比如最開始所說的i++的例子

就會導致執行緒不安全。

關於執行緒安全的詳情使用,請參考以前寫的這篇Blog,或者關注後續系列,也會談到相關內容。

相關推薦

Java併發程式設計學習筆記():Java記憶體模型執行安全

文章目錄 原子性 有序性 可見性 – 編譯器優化 – 硬體優化(如寫吸收,批操作) Java虛擬機器層面的可見性 Happen-Before規則(先行發生) 程式順序原則: volat

[併發Java ] Java記憶體模型執行安全

網上很多資料在描述Java記憶體模型的時候,都會介紹有一個主存,然後每個工作執行緒有自己的工作記憶體。資料在主存中會有一份,在工作記憶體中也有一份。工作記憶體和主存之間會有各種原子操作去進行同步。 但是由於Java版本的不斷演變,記憶體模型也進行了改變。本文只講述Jav

實戰Java併發程式設計之Java記憶體模型執行安全

Java記憶體模型 原子性: 是指一個操作是不可中斷的.即使多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒干擾. 一般CPU的指令是原子的. Q:i++是原子操作嗎? A:不是.

Java執行系列七)Java記憶體模型執行的三大特性

Java記憶體模型和執行緒的三大特性 多執行緒有三大特性:原子性、可見性、有序性 1、Java記憶體模型 Java記憶體模型(Java Memory Model ,JMM),決定一個執行緒對共享變數的寫入時,能對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的

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

Java虛擬機器—記憶體模型與執行緒 Lyon Keep balance,Be a better man! ​關注他 3 人讚了該文章 前言: 本文主要介紹Java的記憶體模型和Java執行緒。 Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在JVM

Java記憶體模型以及執行安全的可見性問題

Java記憶體模型 VS JVM執行時資料區 首先Java記憶體模型(JMM)和JVM執行時資料區並不是一個東西,許多介紹Java記憶體模型的文章描述的堆,方法區,Java虛擬機器棧,本地方法棧,程式計數器這東西並不是Java記憶體模型的內容而是JVM執行時資料區的內容。要理解二者的區別就要了解《Jav

四十八、從JVM記憶體模型執行安全

作為一個三個多月沒有去工作的獨立開發者而言,今天去小米麵試了一把.怎麼說呢,無論你水平如何,請確保在面試之前要做準備,就像其中一位面試官說的一樣,我知道你水平不錯,但是無論如何也是要準備下的,不然你怎麼會連這個方法也忘記了? 此刻,我突然覺得我是一個假程式設計師.為什麼這麼說呢,作為一個從12年

Java 併發第二階段實戰---併發設計模式,記憶體模型,CPU一致性協議,volatile關鍵字剖析

汪文君高併發程式設計第二階段01講-課程大綱及主要內容介紹. 汪文君高併發程式設計第二階段02講-介紹四種Singleton方式的優缺點在多執行緒情況下. 汪文君高併發程式設計第二階段03講-介紹三種高效優雅的Singleton實現方式. 汪文君高併發程式設計第二階段04講-多執行緒的休息室WaitSet詳細

十一、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)