高並發詳解之同步synchronized關鍵字
-
兩種用法:對象鎖和類鎖。
-
多線程訪問同步方法的7種情況:是否是static、Synchronized方法等。
原理:加解鎖原理,可重入原理,可見性原理
缺陷:效率低,不夠靈活,無法預判是否成功獲得到鎖
Synchronized的作用
官方解釋:
同步方法支持一種簡單的策略來防止線程干擾和內存一致性錯誤:如果是一個對象對多個線程可見,則對該對象變量的所有讀取或寫入都是通過該同步方法完成的。
簡單理解:
能夠保證在同一時刻最多只有一個線程執行該段代碼,以達到保證並發安全的效果
(synchronized修飾的代碼以原子的方式執行,鎖的使用)
Synchronized的地位
-
synchronized是java的關鍵字,被java語言原生支持
-
是最基本的同步互斥手段
-
是並發編程中的元老級角色,是並發編程的必學內容
??????不使用並發手段的后果
/**
* @author glong
* @date 2019/9/4 17:01
*
* 描述: 消失的請求數
*/
public class DisappearRequest1 implements Runnable{
static DisappearRequest1 instance = new DisappearRequest1();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
代碼實戰:兩個線程同時a++,最后的結果比預計的少
原因:count++ 看上去是一個操作,實際上是三個動作:
-
1.讀取count,
-
2.將coutn加一,
-
3.將count的值寫入到內存中
/**
* @author glong
* @date 2019/9/4 20:04
* 描述:
*/
public class DisappearRequest2 implements Runnable{
static DisappearRequest2 instance = new DisappearRequest2();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
Synchronized的兩種用法
對象鎖
包括方法鎖(默認鎖對象為this當前實例對象)和同步代碼塊鎖(自己制定鎖對象)
類鎖
指synchronized修飾靜態的方法或指定鎖為Class對象
對象鎖
代碼塊形式:手動指定鎖對象
方法鎖形式:synchronized修飾普通方法,鎖對象默認為this
類鎖
概念(重要):java類可能有很多個對象,但是只有一個Class對象
形式1:synchronized 加在static方法上
形式2:synchronized(*.class)代碼塊
概念:
-
只有一個class對象:java類可能有很多對象,但是只有一個class對象
-
本質:所謂的類鎖,不過是Class對象的鎖而已
-
用法和效果:類鎖只能在同一時刻被一個對象擁有
面試常考
多線程訪問同步方法的7種情況
-
-
兩個線程同時訪問一個對象的同步方法
-
兩個線程同時訪問兩個對象的同步方法
-
兩個線程訪問的是synchronized的靜態方法
-
同時訪問同步方法與非同步方法 --> 非同步方法不受到影響
-
訪問同一個對象的不同的普通同步方法 -->同步方法串行,因為使用的this是同一個對象
-
同時訪問靜態synchronized和非靜態synchronized方法
-
方法拋出異常后,會釋放鎖
(在lock(類)中默認,即便拋出異常,沒有顯示的手動進行釋放是不會釋放鎖的)
(在synchronized關鍵字中,拋出異常會主動釋放鎖)
-
-
一個對象有多個方法鎖時候串行執行
總結
3點核心思想
-
一把鎖只能被一個線程獲取,沒有拿到鎖的線程必須等待(對應1,5情況)
-
每個實例都對應有自己的一把鎖,不同實例之間互不影響;
例外:鎖對象是*.class以及synchronized修飾的是static方法的時候(即鎖是類鎖),所有對象共用同一把鎖(對應2,3,4,6情況);
-
無論是方法正常執行完畢或者方法拋出異常,都會釋放鎖(對應7情況)
原理部分
Synchronized的性質
-
可重入
-
不可中斷(劣勢所在)
一、可重入(也叫遞歸鎖):
什么是可重入:指的是同一線程的外層函數獲得鎖之后,內層函數可以直接再次獲取該鎖(無需排隊)
什么是不可重入:一個線程拿到一把鎖,但是需要再次使用這把鎖,必須釋放以后和其他線程(需要這把鎖的線程)進行競爭排隊
好處:避免死鎖、提高封裝性(不可重入,當線程既想那鎖又不釋放鎖就會永久等待)
粒度: 線程而非調用(用三種情況來說明和pthread的區別)
情況1:證明同一個方法是可重入的
情況2:證明可重入不要求是同一個方法
情況3:證明可重入不要求是同一個類中的
可重入結論
通過證明發現Synchronized的粒度是線程層面的,就是只要拿到了這把鎖,在當前這個線程中都可以使用
二、不可中斷性質
解釋:
一旦這個鎖被別人獲得了,如果我還想獲得,我只能等待或者阻塞,直到別的線程釋放這個鎖如果別人永遠不釋放鎖,那么我只能永遠的等待下去。
對比:
相比之下,未來會介紹的lock類,可以擁有中斷的能力,第一點,如果我覺得我等的時間太長了,有權中斷現在已經獲得到鎖的線程的執行;第二點,如果我覺得我等待的時間太長了不想在等了,也可以退出。
原理
-
加鎖和釋放鎖的原理:現象、時機、深入jvm看字節碼
-
可重入原理:加鎖次數計數器
-
保證可見性的原理:內存模型
一、加鎖和釋放鎖的原理
-
現象
每一個類的實例對應一把鎖,而每一個synchronized方法都必須首先獲得調用該方法的類的實例的鎖才能執行。否則線程就會阻塞,而方法一旦執行,就會獨占這把鎖,直到該方法返回,或者拋出異常,才能釋放鎖。釋放之后,其他被阻塞的線程才能獲得這把鎖,重新進入可執行的狀態。
-
獲取和釋放鎖的實際:內置鎖(時機)
每一個java對象都可以用作一個實現同步的鎖(內置鎖或者叫做監視器鎖monitor lock),線程在進入代碼塊之前會自動進入這把鎖,並且在退出這個同步代碼塊的時候會自動釋放鎖,正常退出和異常退出都會釋放。獲得這個內置鎖的唯一途徑就是進入到這個鎖所保護的同步代碼塊或者方法中
-
等價代碼
// method1和method2兩個方法等價
// 代碼來源:java並發實戰
public synchronized void method1(){
System.out.println("我是Synchronized形式的鎖");
}
public void method2(){
// 鎖住
lock.lock();
try {
System.out.println("我是lock形式的鎖");
}finally {
lock.unlock();
}
} -
深入 JVM看字節碼:反編譯、monitor指令
加鎖和釋放鎖的原理
深入jvm看字節碼
java對象頭有一個部分就是存儲synchronized鎖的
當線程訪問同步代碼塊的時候就必須得到這把鎖,退出的時候必須釋放這把鎖,這個鎖存放在java對象頭中的
進入鎖和釋放鎖是基於monitor對象來實現同步方法和同步代碼塊
monitor重要的兩個指令:monitorenter(插入到同步代碼塊開始的時候)monitorexit(插入同步代碼塊結束的時候和退出的時候)
一個monitorenter可以對應多個monitorexit,是因為進入之后度與退出的情況並不是一一對應的,多種退出方式使得exit數量可能大於enter的數量。
monitorenter使monitor計數器+1,monitorexit使計數器-1,如果變成沒有變成0,說明之前是重入的,那么線程繼續持有鎖
當訪問到monitorenter的時候就嘗試獲取這個對象所對應的monitor所有權(這個對象鎖)
一個monitor的lock鎖只能被一個線程在同一時間獲取,一個對象在獲取monitor會出現以下三種情況:
-
如果monitor計數器為0,說明目前還沒有被獲得,該現場立刻獲得,並把monitor計數器+1(成功獲得鎖)
-
如果monitor已經拿到了鎖的所有權,又重入了,計數器累加,再加一
-
如果monitor已經被其他線程獲取了,我獲取的時候則獲取不了,進入阻塞,直到monitor為0
monitorexit 執行的時候會使monitor計數器-1,當monitor計數器為0,釋放鎖,不為0說明是可重入進來的,繼續持有這把鎖。
當計數器為0,其他的被阻塞的線程將會重新嘗試獲取該鎖的所有權
synchronized關鍵字底層原理:monitorentrant進入鎖,monitorexit退出鎖
類鎖可以理解為一種特殊的對象鎖,鎖住的是類所對應的.class對象
靜態方法鎖是類鎖
1、一個線程釋放鎖,JVM如何決定下一個獲取該鎖的線程:
有可能是新來的獲取到,有可能是老的處於阻塞狀態的線程獲取到,
隨機,不公平的
2、synchronized使得同時只有一個線程可以執行,性能較差,有什么辦法可以提升性能?
優化鎖的使用范圍,需要加鎖的地方才加鎖
可重入原理:加鎖次數計數器
-
JVM負責跟蹤對象(每個對象自動含有一把鎖)被加鎖的次數
-
線程第一次給對象加鎖的時候,計數器為1.每當這個相同的線程在此對象上再次獲得鎖時,,計數回遞增
-
每當任務離開的時候,計數遞減,當計數為0的時候,鎖被完全釋放。
可見性原理:java內存模型

線程A與線程B通信:(兩個步驟)
-
線程A把副本寫到主存中,更新主內存(線程內存 -->主內存)
-
線程B從主內容讀取數據(JVM執行)
sychnorized的可見性
一旦一個代碼塊或者方法被我們的synchronize關鍵字所修飾,他在執行完畢之后,被鎖住的對象所做的任何修改都要在釋放鎖之前,從線程內存寫會到主內存,也就是不會存在線程內存和主內存內容不一致的情況,同樣在進入代碼塊得到鎖之后,被鎖定的對象的數據也是直接從主內存中讀取出來的,而在釋放的時候會把修改的內容寫回到主內存中,所以從主內存中讀取到的數據一定是最新的,通過這個原理synchronize保證了我們每一次的執行都是可靠的,保證了可見性
Synchronized的缺陷
-
效率低:鎖的釋放情況少、試圖獲得鎖時不能設定超時、不能中斷一個正在試圖獲得鎖的線程。
-
兩種釋放方式,一種是正常執行任務完釋放,一種是異常JVM釋放
-
不能設置超時,只能一直等待
-
-
不夠靈活(讀寫鎖更靈活):加鎖和釋放的時機單一,每個鎖僅有單一的條件(某個對象),可能是不夠的。
-
無法知道是否成功獲取到鎖。(無法判斷狀態)
Lock
-
lock();//獲取鎖
-
unlock();//釋放鎖
-
tryLock();//判斷鎖是否可用。返回值為:boolean;
-
tryLock(time,TimeUnit);//在規定的時間內,如果未獲得鎖,則就放棄。第一項表示規定的時間;第二項表示設置時間的單位
面試
-
synchronized關鍵字使用的注意事項
-
使用注意點:鎖對象不能為空,作用域不宜過大,避免死鎖
-
鎖的信息保持在對象頭中(沒有對象就沒有對象頭)
-
鎖對象必須是一個實例對象
-
-
如何選擇Lock和synchronized關鍵字
1)建議都不使用,可以使用java.util.concurrent包中的Automic類、countDown等類
2)優先使用現成工具,如果沒有就優先使用synchronized關鍵字,好處是寫勁量少的代碼就能實現功能。如果需要靈活的加解鎖機制,則使用Lock接口
3)如果synchronized在程序中適用,優先使用這個關鍵字,這樣可以減少需要編寫的代碼,減少出錯的幾率
-
多線程訪問同步方法的各種具體情況
7種情況
思考題
-
在多個線程等待同一個synchronized鎖的時候,JVM是如何選擇下一個獲取鎖的是那個線程?
---》》涉及內部鎖調度機制,線程有,進程也有調度機制
-
Synchronized是的同時只有一個線程可以執行,性能較差,有什么辦法可以提升性能?
---》》一、優化使用范圍;二、使用其他類型的lock
-
想更加靈活的控制鎖的獲取和釋放(現在釋放鎖的時機都被規定死了),怎么辦?
---》》
-
什么是鎖的升級、降級?什么是JVM里的偏斜鎖、輕量級鎖、重量級鎖?
總結
一句話介紹synchronized
-
JVM會自動通過使用monitor來加鎖和解鎖,保證了同時只有一個線程可以執行指定代碼,從而保證了線程安全,同時具有可重入和不可中斷的性質
