1. 程式人生 > >Java高併發程式設計學習筆記(三):Java記憶體模型和執行緒安全

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

文章目錄


原子性

原子性是指一個操作是不可中斷的。即使是在多個執行緒一起執行的時候,一個操作一旦開始,就 不會被其它執行緒干擾。
i++是原子操作嗎?
不是,因為包含了三次操作:讀i,i+1,新值寫到i中。
比如i=1,i是static的,一個執行緒a讀到了1,另一個執行緒b線上程一做加法之前也讀到了i=1, a執行緒和b執行緒同時拿到了i,做i++的操作,a執行緒i++後變成了2,b執行緒i++後也變成了2, 所以最後的i的值是2,但是實際上i的值應該是3

有序性

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

class
OrderExample { int a = 0; boolean flag = false; public void writer() { a = 1; flag = true; } public void reader() { if (flag) { inti= a+1; ...... } }

在這裡插入圖片描述

一條指令(彙編指令)的執行是可以分為很多步驟的
– 取指IF (把指令取出來)
– 譯碼和取暫存器運算元 ID (引數取出來)
– 執行或者有效地址計算 EX (執行)
– 儲存器訪問 MEM (儲存器訪問)
– 寫回WB (資料寫會到暫存器中去)
注意:每一個部分會佔用計算機不同的硬體


在這裡插入圖片描述
複雜一點的:
在這裡插入圖片描述
發現加了很多的氣泡進去
在這裡插入圖片描述
在這裡插入圖片描述

可見性

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

– 編譯器優化

比如上面的重排,並不知道另一個執行緒中的值是多少,或者編譯期,一個執行緒中,一個值優化到了某個暫存器中,另一個執行緒中將這個值放到了快取記憶體cache中,這兩個執行緒就不能再同一時間知道對方修改了值。多核cpu,每一個cpu中都有自己的暫存器,變數被不同的cpu不同的暫存器不同的cache中儲存,所以不能保證可見。

– 硬體優化(如寫吸收,批操作)

cpu想把資料寫到記憶體裡的時候,很可能不會是直接把資料寫到記憶體裡面,因為這樣很慢,先把資料寫到硬體佇列裡面,然後通過批量操作的方式批量寫到記憶體裡面去,這樣會比較快一些,還會做優化,比如對同一個記憶體地址多次做了不同的讀寫,認為是沒有必要,因為是以最後一個為準,所以乾脆就把老的讀寫,就不讀寫進去,只將最後的地址讀寫進去
在這裡插入圖片描述
如果不做優化,就不會有這些問題,可是不做優化的話,效能就會很差。

Java虛擬機器層面的可見性

博文:http://hushi55.github.io/2015/01/05/volatile-assembly

public class VisibilityTest extends Thread { private boolean stop;
public void run() {
int i = 0; while(!stop) {
i++; }
System.out.println("finish loop,i=" + i); }
public void stopIt() { stop = true;
}
public boolean getStop(){ return stop;
}

public static void main(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());
DATAGURU專業資料分析社群
}

就是希望在v.stopIt();之後讓stop=true,輸出System.out.println(“finish loop,i=” + i); }
可是實際的操作,是並沒有輸出這句話的。

在這裡插入圖片描述
如何檢視是什麼模式?
在這裡插入圖片描述
虛擬機器執行有兩種方式client方式和server模式,client不會做太多的優化,就是系統啟動的比較快,server模式系統啟動的慢,但是有很多的優化,現在64位的機器都是server模式。
通過server模式發現是永遠不會執行完。
如何進行檢視彙編指令 ?
1、可以使用命令

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Main
(Main是class檔案)

2、在IDEA配置VM options,列印彙編指令,如下圖。

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

原文:https://blog.csdn.net/ljheee/article/details/82218156

上圖是部落格作者整理的彙編程式碼,這裡解釋一下:
因為jvm的內部優化,導致不斷的在紅色的程式碼部分進行迴圈,並沒有走!stop的這個而判斷指令,這個判斷只是在剛剛進來的時候回進行了一次判斷,所以會不斷的執行下去。也就出現了上面的結果。

引用博文中的一句話:
程式比較簡單,在主執行緒中啟動一個執行緒,這個執行緒不停的對區域性變數做自增操作,主執行緒休眠 1 秒中後改變啟動執行緒的迴圈控制變數,想讓它停止迴圈。這個程式在 client 模式下是能停止執行緒做自增操作的,但是在 server 模式先將是無限迴圈。若是改成

private volatile boolean stop;

用 volatile 修飾 stop 變數,將不會出現死迴圈。
我們知道 volatile 在 JVM 記憶體模型中是保證修飾變數的可見性,這個不是我們今天討論的重點,我們今天想看看在 volatile 修飾下和不修飾程式碼編譯成的彙編程式碼的區別,以便我們學習 JVM 的記憶體模型。

再來看一個例子

在這裡插入圖片描述

在這裡插入圖片描述
上圖是從java語言規範中拿到的,描述可見性和指令重排的一些問題

Happen-Before規則(先行發生)

程式順序原則:

一個執行緒內保證語義的序列性
對於單執行緒來說,重排前和重排後的結果必須一致
在這裡插入圖片描述

volatile規則:

volatile變數的寫,先發生於讀,這保證了volatile變數的可見性

鎖規則:

解鎖(unlock)必然發生在隨後的加鎖(lock)前
如果加鎖被重排到解鎖前面,因為還沒有解鎖,肯定是獲取不到鎖的

傳遞性:

A先於B,B先於C,那麼A必然先於C

執行緒的start()方法先於它的每一個動作

執行緒的所有操作先於執行緒的終結(Thread.join())

執行緒的中斷(interrupt())先於被中斷執行緒的程式碼

物件的建構函式執行結束先於finalize()方法

執行緒安全的概念

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

i++在多執行緒下訪問的情況
在這裡插入圖片描述
i++是static的一個變數,在多執行緒中不是執行緒安全的,一個執行緒在讀的時候,另一個執行緒也在讀,一個執行緒在寫的時候,另一個執行緒也在寫,所以寫和讀的時候值會被另外一個執行緒覆蓋掉。甚至執行緒很多的時候,i可能會越加越小,

解決:阻塞的方式

public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync(); static int i=0;
@Override
public void run() {
for(int j=0;j<10000000;j++){ synchronized(instance){
  } }