
目錄
- 為什么要并發編程
- 并發編程帶來的問題
- 安全性問題
- 原子性問題
- 可見性問題
- 有序性問題
- 活躍性問題
- 死鎖
- 活鎖
- 饑餓
- 性能問題
- 執行緒生命周期
- 管程
- synchronized
- volatile
- final
為什么要并發編程

最主要還是壓榨硬體(上圖為我cpu的使用率),現在硬體都是過剩的狀態,不壓榨干嘛,也不能天天指望程式員拿頭發來優化演算法啊,
并發編程帶來的問題
安全性問題
都知道并發編程能提高效率,但是這肯定有代價的,會帶來很多問題主要分為3大類,
原子性問題
-
原子性:一個或多個操作在cpu執行程序中不可分割,不可分割也就是中間狀態對外不可見,所以只要保證對外不可見即可,
-
為什么會有這個問題?

cpu會類似上圖每隔一定時間鐵環執行緒執行從而達到,多個程式同時在執行的感覺,也就是說你的程式執行到一半,就可能切換走了,java是種高級語言,一行往往對應多條cpu指令,加上cpu指令沖排序,很可能結果線出來,但是程序還沒完成,一旦被外界拿去用,就會出現問題,比如Object o=new Object();這一部操作對應了以下3步,- 申請記憶體,賦值默認值
- 成員變數初始化
- 賦值給物件參考
如果這個程序對外不可見,隨便怎么重拍都無所謂,但是如果2和3掉了個位置,別人用的時候發現沒有初始化,很可能就會出現問題,這還只是一行命令,就已經出現了風險,多行陳述句出問題的概率更大
-
如何解決?
- 加鎖
可見性問題
- 可見性:一個執行緒對一個值的修改另外一個執行緒可以立馬看見
- 為什么會有這個問題?

記憶體速度相對于cpu而言慢很多,就出現了cpu快取,每個核都有自己的cpu快取,如果多個cpu核處理同一個變數,處理完成之后沒有及時刷回主存并通知其他執行緒重新讀取就會導致可見性問題, - 如何解決?
- voliate
- 加鎖
有序性問題
- 有序性:程式按照代碼的先后順序執行
- 為什么會有這個問題?
前面也說了高級語言一行往往對應多條cpu指令,編譯器為了優化性能,有時候會改變程式中陳述句的先后順序,指令集并行的重排序是對CPU的性能優化,從指令的執行角度來說一條指令可以分為多個步驟完成,如下:- 取指 IF
- 譯碼和取暫存器運算元 ID
- 執行或者有效地址計算 EX (ALU邏輯計算單元)
- 存盤器訪問 MEM
- 寫回 WB (暫存器)

x代表在這里停頓了下,應為R2資料還沒有準備好,所以在這里等待了一會,下面我們看另外一個情況
a=b+c
d=e-f

這會有很多停頓,對這些指令稍微重拍下就可以解決這些停頓,提高cpu利用率


在不影響結果的前提下,只是做了指令重排,但是效率提高了,但也不能為了減少停頓進行排序降低亂排序,JMM通過happens-before保證了可見性保證, - 程式順序規則:一個執行緒中的每個操作,happens-before于該執行緒中的任意后續操作,
- 監視器鎖規則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖,
- volatile變數規則:對一個volatile域的寫,happens-before于任意后續對這個volatile域的讀,
- 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C,
- start()規則:如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那么A執行緒的ThreadB.start()操作happens-before于執行緒B中的任意操作,
- Join()規則:如果執行緒A執行操作ThreadB.join()并成功回傳,那么執行緒B中的任意操作happens-before于執行緒A從ThreadB.join()操作成功回傳,
- 程式中斷規則:對執行緒interrupted()方法的呼叫先行于被中斷執行緒的代碼檢測到中斷時間的發生,
- 物件finalize規則:一個物件的初始化完成(建構式執行結束)先行于發生它的finalize()方法的開始,
- 如何解決?
- voliate
- 加鎖
- final
活躍性問題
死鎖
一組互相競爭資源的執行緒因互相等待,導致“永久”阻塞的現象,并發程式一旦死鎖,一般沒有特別好的方法,很多時候我們只能重啟應用,因此,解決死鎖問題最好的辦法還是規避死鎖,
- 互斥,共享資源 X 和 Y 只能被一個執行緒占用;
- 占有且等待,執行緒 T1 已經取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;
- 不可搶占,其他執行緒不能強行搶占執行緒 T1 占有的資源;
- 回圈等待,執行緒 T1 等待執行緒 T2 占有的資源,執行緒 T2 等待執行緒 T1 占有的資源,就是回圈等待,
活鎖
有時執行緒雖然沒有發生阻塞,但仍然會存在執行不下去的情況,這就是所謂的“活鎖”
饑餓
所謂“饑餓”指的是執行緒因無法訪問所需資源而無法執行下去的情況,優先級低的執行緒得到執行的機會很小,就可能發生執行緒“饑餓”;持有鎖的執行緒,如果執行的時間過長,也可能導致“饑餓”問題,
性能問題
不能適當的發揮多執行緒性能的優勢,不能為了用多執行緒而用多執行緒,因為用鎖會不可避免帶來一點性能問題,很可能在某些情況下很可能執行時間還不如多執行緒,我們之所以使用多執行緒搞并發程式,為的就是提升性能,
- 使用無鎖優化
- 減少鎖的持有時間
- 減少鎖的粒度
- 使用讀寫鎖分離鎖來替換獨占鎖
- 鎖分離
- 鎖粗化
執行緒生命周期

管程
java多執行緒基本上都是基于Monitor實作的
管程博客這個博客寫的不錯建議看這個

synchronized
synchronized首先說下如何使用,作用于方法,作用于同步代碼塊,當寫在靜態方法中,鎖的是Class物件,和同步代碼塊中寫(xxx.class)效果一致,下面有個測驗代碼,感興趣的可以測驗下
public class SynchronizedDemo {
public synchronized void test1() throws InterruptedException {
System.out.println("test1");
Thread.sleep(10000);
System.out.println("test1 end");
test3();
}
public static synchronized void test2() throws InterruptedException {
System.out.println("test2");
Thread.sleep(10000);
System.out.println("test2 end");
}
public void test3() throws InterruptedException {
synchronized (this){
System.out.println("test3");
Thread.sleep(10000);
System.out.println("test3 end");
}
}
public void test4() throws InterruptedException {
synchronized (SynchronizedDemo.class){
System.out.println("test4");
Thread.sleep(10000);
System.out.println("test4 end");
test2();
}
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
Runnable r=new Runnable() {
@Override
public void run() {
try {
// synchronizedDemo.test1();
synchronizedDemo.test2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable r1=new Runnable() {
@Override
public void run() {
try {
// synchronizedDemo.test3();
synchronizedDemo.test4();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1=new Thread(r);
Thread t2=new Thread(r1);
// t1.start();
t2.start();
}
}
synchronized用的鎖其實是存在java物件頭中,jvm中采用2個字來存盤物件頭(如果物件是陣列則會分配3個字,多出來的1個字記錄的是陣列長度),其主要結構是由Mark Word 和 Class Metadata Address 組成,其結構說明如下表:
| 虛擬機位數 | 頭物件結構 | 說明 |
|---|---|---|
| 32/64bit | Mark Word | 存盤物件的hashCode、鎖資訊或分代年齡或GC標志等資訊 |
| 32/64bit | Class Metadata | Address 型別指標指向物件的類元資料,JVM通過這個指標確定該物件是哪個類的實體, |
Mark Word在不同的鎖狀態下存盤的內容不同
當只有一個執行緒獲取鎖時,偏向鎖的標識改成1,執行緒id,時間戳,當有其他執行緒嘗試獲取鎖的時候,會判斷當前執行緒id和markword里面的執行緒id是否是一致,不一致,膨脹為輕量級鎖,然后自旋比較,還沒有拿到鎖,就膨脹為重量級鎖,重量級鎖的指標就指向了一個monitor物件,結構如下
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //處于wait狀態的執行緒,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處于等待鎖block狀態的執行緒,會被加入到該串列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- _owner:指向持有ObjectMonitor物件的執行緒
- _WaitSet:存放處于wait狀態的執行緒佇列
- _EntryList:存放處于等待鎖block狀態的執行緒佇列
- _recursions:鎖的重入次數
- _count:用來記錄該執行緒獲取鎖的次數
當多個執行緒同時訪問一段同步代碼時,首先會進入_EntryList佇列中,當某個執行緒獲取到物件的monitor后進入_Owner區域并把monitor中的_owner變數設定為當前執行緒,同時monitor中的計數器_count加1,即獲得物件鎖,
若持有monitor的執行緒呼叫wait()方法,將釋放當前持有的monitor,_owner變數恢復為null,_count自減1,同時該執行緒進入_WaitSet集合中等待被喚醒,若當前執行緒執行完畢也將釋放monitor(鎖)并復位變數的值,以便其他執行緒進入獲取monitor(鎖),如下圖所示
volatile
該關鍵字確保了對一個變數的更新對其他執行緒可見,當一個變數被宣告為volatile時候,執行緒寫入時候不會把值快取在暫存器或者或者在其他地方,當執行緒讀取的時候會從主記憶體重新獲取最新值,而不是使用當前執行緒的拷貝記憶體變數值,volatile雖然提供了可見性保證,但是不能使用他來構建復合的原子性操作,也就是說當一個變數依賴其他變數或者更新變數值時候新值依賴當前老值時候不在適用,

如圖執行緒A修改了volatile變數b的值,然后執行緒B讀取了改變數值,那么所有A執行緒在寫入變數b值前可見的變數值,在B讀取volatile變數b后對執行緒B都是可見的,圖中執行緒B對A操作的變數a,b的值都可見的,volatile的記憶體語意和synchronized有類似之處,具體說是說當執行緒寫入了volatile變數值就等價于執行緒退出synchronized同步塊(會把寫入到本地記憶體的變數值同步到主記憶體),讀取volatile變數值就相當于進入同步塊(會先清空本地記憶體變數值,從主記憶體獲取最新值),轉自
final
final基礎用法想必大家都已經了解了,他在多執行緒中的比較重要的2個點,
- 在建構式中對final域寫入,隨后再把這個變數賦值給一個參考變數,這兩個不能重排
- 讀取物件的參考和讀取這個final域,兩個操作之間不能重排(大部分處理器是這樣的,但是有少部分處理器抽風)
參考書籍
《實戰Java高并發程式設計》
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/1409.html
標籤:python
上一篇:mac中Typora+PicGo圖床+gitee 保姆級教程
下一篇:JWT的基本介紹
