Java高併發程序設計學習筆記(三):Java內存模型和線程安全

 

轉自:https://blog.csdn.net/dataiyangu/article/details/86412704java

原子性
有序性
可見性
– 編譯器優化
– 硬件優化(如寫吸取,批操做)
Java虛擬機層面的可見性
Happen-Before規則(先行發生)
程序順序原則:
volatile規則:
鎖規則:
傳遞性:
線程的start()方法先於它的每個動做
線程的全部操做先於線程的終結(Thread.join())
線程的中斷(interrupt())先於被中斷線程的代碼
對象的構造函數執行結束先於finalize()方法
線程安全的概念
原子性
原子性是指一個操做是不可中斷的。即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就 不會被其它線程干擾。
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的值應該是3git

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

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 (數據寫會到寄存器中去)
注意:每個部分會佔用計算機不一樣的硬件 安全

複雜一點的:多線程

發現加了不少的氣泡進去併發

可見性
可見性是指當一個線程修改了某一個共享變量的值,其餘線程是否可以當即知道這個修改。app

– 編譯器優化
好比上面的重排,並不知道另外一個線程中的值是多少,或者編譯期,一個線程中,一個值優化到了某個寄存器中,另外一個線程中將這個值放到了高速緩存cache中,這兩個線程就不能再同一時間知道對方修改了值。多核cpu,每個cpu中都有本身的寄存器,變量被不一樣的cpu不一樣的寄存器不一樣的cache中保存,因此不能保證可見。jvm

– 硬件優化(如寫吸取,批操做)
cpu想把數據寫到內存裏的時候,極可能不會是直接把數據寫到內存裏面,由於這樣很慢,先把數據寫到硬件隊列裏面,而後經過批量操做的方式批量寫到內存裏面去,這樣會比較快一些,還會作優化,好比對同一個內存地址屢次作了不一樣的讀寫,認爲是沒有必要,由於是以最後一個爲準,因此乾脆就把老的讀寫,就不讀寫進去,只將最後的地址讀寫進去
ide

若是不作優化,就不會有這些問題,但是不作優化的話,性能就會不好。

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模式發現是永遠不會執行完。
如何進行查看彙編指令 ?
一、可使用命令

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Main
(Main是class文件)

二、在IDEA配置VM options,打印彙編指令,以下圖。

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

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

上圖是博客做者整理的彙編代碼,這裏解釋一下:
由於jvm的內部優化,致使不斷的在紅色的代碼部分進行循環,並無走!stop的這個而判斷指令,這個判斷只是在剛剛進來的時候回進行了一次判斷,因此會不斷的執行下去。也就出現了上面的結果。

引用博文中的一句話:
程序比較簡單,在主線程中啓動一個線程,這個線程不停的對局部變量作自增操做,主線程休眠 1 秒中後改變啓動線程的循環控制變量,想讓它中止循環。這個程序在 client 模式下是能中止線程作自增操做的,可是在 server 模式先將是無限循環。如果改爲

private volatile boolean stop;
1
用 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 impl

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

相關文章
相關標籤/搜索