多線程訪問共享資源的時候,避免不了資源競爭而導致數據錯亂的問題,所以我們通常為了解決這一問題,都會在訪問共享資源之前加鎖。不同種類有不同的成本開銷,不同的鎖適用于不同的場景。
從資源已被鎖定,線程是否阻塞可以分為 自旋鎖(spinlock)和互斥鎖(mutexlock);
從線程是否需要對資源加鎖可以分為 悲觀鎖 和 樂觀鎖;
從多個線程并發訪問資源,也就是 Synchronized 可以分為 無鎖、偏向鎖、 輕量級鎖 和 重量級鎖;
從鎖的公平性進行區分,可以分為公平鎖 和 非公平鎖;
從根據鎖是否重復獲取可以分為可重入鎖(自己獲得鎖以后,自己還可以進入鎖之中) 和 不可重入鎖;
從那個多個線程能否獲取同一把鎖分為共享鎖和 排他鎖;
互斥鎖是在訪問共享資源之前對其進行加鎖操作,在訪問完成之后進行解鎖操作。加鎖后,任何其它試圖再次加鎖的線程都會被阻塞,直到當前線程解鎖。在這種方式下,只有一個線程能夠訪問被互斥鎖保護的資源。如synchronized/Lock 這些方式都是互斥鎖,不同線程不能同時進入 synchronized Lock 設定鎖的范圍
自旋鎖是一種特殊的互斥鎖,當資源被加鎖后,其它線程想要再次加鎖,此時該線程不會被阻塞睡眠而是陷入循環等待狀態(CPU不能做其它事情),循環檢查資源持有者是否已經釋放了資源,這樣做的好處是減少了線程從睡眠到喚醒的資源消耗,但會一直占用CPU的資源。
區別:互斥鎖的起始開銷要高于自旋鎖,但是基本上是一勞永逸,臨界區持鎖時間的大小并不會對互斥鎖的開銷造成影響,而自旋鎖是死循環檢測,加鎖全程消耗cpu,起始開銷雖然低于互斥鎖,但是隨著持鎖時間,加鎖的開銷是線性增長。
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。
讀寫鎖也叫共享鎖。其共享是在讀數據的時候,可以讓多個線程同時進行讀操作的。在寫的時候具有排他性,其他讀或者寫操作都要被阻塞。
1. 悲觀鎖
線程對一個共享變量進行訪問,它就自動加鎖,所以只能有一個線程訪問它
悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。
缺點:只有一個線程對它操作時,沒有必要加鎖,造成了性能浪費
2.樂觀鎖
線程訪問共享變量時不加鎖,當執行完后,同步值到內存時,使用舊值和內存中的值進行判斷,如果相同,那么寫入,如果不相同,重新使用新值執行。樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。
缺點:值相同的情況,可能被其他線程執行過;操作變量頻繁時,重新執行次數多,造成性能浪費;完成比較后,寫入前,被其他線程修改了值,導致不同步問題
1)synchronized 同步語句塊的情況
public class SynchronizedDemo {
?? ?public void method() {
?? ??? ?synchronized (this) {
?? ??? ??? ?System.out.println("synchronized 代碼塊");
?? ??? ?}
?? ?}
}
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息:首先切換到類的對應目錄執行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件,然后執行javap -c -s -v -l SynchronizedDemo.class。
從上面我們可以看出:synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 對象監視器 monitor 的持有權。
在 Java 虛擬機(HotSpot)中,Monitor 是基于 C++實現的,由ObjectMonitor實現的。每個對象中都內置了一個 ObjectMonitor對象。另外,wait/notify等方法也依賴于monitor對象,這就是為什么只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。
在執行monitorenter時,會嘗試獲取對象的鎖,如果鎖的計數器為 0 則表示鎖可以被獲取,獲取后將鎖計數器設為 1 也就是加 1。
在執行 monitorexit 指令后,將鎖計數器設為 0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
2) synchronized 修飾方法的的情況
public class SynchronizedDemo2 {
?? ?public synchronized void method() {
?? ??? ?System.out.println("synchronized 方法");
?? ?}
}
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息:首先切換到類的對應目錄執行 javac SynchronizedDemo2.java 命令生成編譯后的 .class 文件,然后執行javap -c -s -v -l SynchronizedDemo2.class。
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明了該方法是一個同步方法。JVM 通過該 ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。
public class Singleton {
// 這里為什么需要加上volatile 后面會講解
private volatile static Singleton uniqueInstance;
// 私有化構造方法
private Singleton() {
}
// 提供getInstance方法
public static Singleton getInstance() {
//先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼
if (uniqueInstance == null) {
//類對象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
其中uniqueInstance 變量采用 volatile 關鍵字修飾,分析如下:
uniqueInstance = new Singleton(); 這段代碼其實是分為三步執行:
1.為 uniqueInstance 分配內存空間
2.初始化 uniqueInstance
3.將 uniqueInstance 指向分配的內存地址
但是由于 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出現問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 后發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
可重入原理即加鎖次數計數器。一個線程拿到鎖之后,可以繼續地持有鎖,如果想再次進入由這把鎖控制的方法,那么它可以直接進入。它的原理是利用加鎖次數計數器來實現的。
1.每重入一次,計數器+1
每個對象自動含有一把鎖,JVM負責跟蹤對象被加鎖的次數。
線程第一次給對象加鎖的時候,計數器=0+1=1,每當這個相同的線程在此對象上再次獲得鎖時,計數器再+1。只有首先獲取這把鎖的線程,才能繼續在這個對象上多次地獲取這把鎖
2.計數器-1
每當任務結束離開時,計數遞減,當計數器減為0,鎖被完全釋放。
利用這個計數器可以得知這把鎖是被當前多次持有,還是如果=0的話就是完全釋放了。
不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。因為非靜態方法上的 synchronized 修飾符要求執行方法時要獲得對象的鎖,如果已經進入A 方法說明對象鎖已經被取走,那么試圖進入 B 方法的線程就只能在等鎖池(注意不是等待池哦)中等待對象的鎖。
1)lock是一個接口,而synchronized是java的一個關鍵字。
2)synchronized在發生異常時會自動釋放占有的鎖,因此不會出現死鎖;而lock發生異常時,不會主動釋放占有的鎖,必須手動來釋放鎖,可能引起死鎖的發生。
synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別。既然ReentrantLock是類,那么它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量,ReentrantLock比synchronized的擴展性體現在幾點上:
(1)ReentrantLock可以對獲取鎖的等待時間進行設置,這樣就避免了死鎖
(2)ReentrantLock可以獲取各種鎖的信息
(3)ReentrantLock可以靈活地實現多路通知
另外,二者的鎖機制其實也是不一樣的。ReentrantLock底層調用的是Unsafe的park方法加鎖,synchronized操作的應該是對象頭中mark word,這點我不能確定。
1)synchronized保證內存可見性和操作的原子性
2)volatile只能保證內存可見性
3)volatile不需要加鎖,比Synchronized更輕量級,并不會阻塞線程(volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。)
4)volatile標記的變量不會被編譯器優化,而synchronized標記的變量可以被編譯器優化(如編譯器重排序的優化).
5)volatile是變量修飾符,僅能用于變量,而synchronized是一個方法或塊的修飾符。
volatile本質是在告訴JVM當前變量在寄存器中的值是不確定的,使用前,需要先從主存中讀取,因此可以實現可見性。而對n=n+1,n++等操作時,volatile關鍵字將失效,不能起到像synchronized一樣的線程同步(原子性)的效果。
1. volatile 修飾變量
2. synchronized 修飾修改變量的方法
3. wait/notify
4. 輪詢
監視器和鎖在Java虛擬機中是一塊使用的。監視器監視一塊同步代碼塊,確保一次只有一個線程執行同步代碼塊。每一個監視器都和一個對象引用相關聯。線程在獲取鎖之前不允許執行同步代碼。
在 java 虛擬機中, 每個對象( Object 和 class )通過某種邏輯關聯監視器,每個監視器和一個對象引用相關聯, 為了實現監視器的互斥功能, 每個對象都關聯著一把鎖.一旦方法或者代碼塊被 synchronized 修飾, 那么這個部分就放入了監視器的監視區域, 確保一次只能有一個線程執行該部分的代碼, 線程在獲取鎖之前不允許執行該部分的代碼。另外 java 還提供了顯式監視器( Lock )和隱式監視器( synchronized )兩種鎖方案。
死鎖 : 指多個線程在運行過程中因爭奪資源而造成的一種僵局。比如有一個線程A,按照先鎖a再獲得鎖b的的順序獲得鎖,而在此同時又有另外一個線程B,按照先鎖b再鎖a的順序獲得鎖。
死鎖發生的必要條件
(1) 互斥,同一時刻只能有一個線程訪問。
(2) 持有且等待,當線程持有資源A時,再去競爭資源B并不會釋放資源A。
(3) 不可搶占,線程T1占有資源A,其他線程不能強制搶占。
(4) 循環等待,線程T1占有資源A,再去搶占資源B如果沒有搶占到會一直等待下去。
想要破壞死鎖那么上訴條件只要不滿足一個即可,那么分析如下
(1) 互斥條件,不可破壞,如果破壞那么并發安全就不存在了。
(2) 持有且等待,可以破壞,可以一次性申請所有的資源。
(3) 不可搶占,當線程T1持有資源A再次獲取資源B時,發現資源B被占用那么主動釋放資源A。
(4) 循環等待,可以將資源排序,可以按照排序順序的資源申請,這樣就不會存在環形資源申請了。
活鎖:是指線程1可以使用資源,但它很禮貌,讓其他線程先使用資源,線程2也可以使用資源,但它很紳士,也讓其他線程先使用資源。這樣你讓我,我讓你,最后兩個線程都無法使用資源。
就類似馬路中間有條小橋,只能容納一輛車經過,橋兩頭開來兩輛車A和B,A比較禮貌,示意B先過,B也比較禮貌,示意A先過,結果兩人一直謙讓誰也過不去。
饑餓:是指如果線程T1占用了資源R,線程T2又請求封鎖R,于是T2等待。T3也請求資源R,當T1釋放了R上的封鎖后,系統首先批準了T3的請求,T2仍然等待。然后T4又請求封鎖R,當T3釋放了R上的封鎖之后,系統又批準了T4的請求…,T2可能永遠等待。
類似有兩條道A和B上都堵滿了車輛,其中A道堵的時間最長,B相對相對堵的時間較短,這時,前面道路已疏通,交警按照最佳分配原則,示意B道上車輛先過,B道路上過了一輛又一輛,A道上排隊時間最長的確沒法通過,只能等B道上沒有車輛通過的時候再等交警發指令讓A道依次通過。
活鎖和死鎖類似,不同之處在于處于活鎖的線程或進程的狀態是不斷改變的,活鎖可以認為是一種特殊的饑餓。一個現實的活鎖例子是兩個人在狹小的走廊碰到,兩個人都試著避讓對方好讓彼此通過,但是因為避讓的方向都一樣導致最后誰都不能通過走廊。簡單的說就是,活鎖和死鎖的主要區別是前者進程的狀態可以改變但是卻不能繼續執行。
饑餓與死鎖有一定聯系:二者都是由于競爭資源而引起的,但又有明顯差別,主要表現在如下幾個方面:
(1)從進程狀態考慮,死鎖進程都處于等待狀態,忙式等待(處于運行或就緒狀態)的進程并非處于等待狀態,但卻可能被餓死;
(2)死鎖進程等待永遠不會被釋放的資源,餓死進程等待會被釋放但卻不會分配給自己的資源,表現為等待時限沒有上界(排隊等待或忙式等待);
(3)死鎖一定發生了循環等待,而餓死則不然。這也表明通過資源分配圖可以檢測死鎖存在與否,但卻不能檢測是否有進程餓死;
(4)死鎖一定涉及多個進程,而饑餓或被餓死的進程可能只有一個。饑餓和餓死與資源分配策略有關,因而防止饑餓與餓死可從公平性考慮,確保所有進程不被忽視,如FCFS分配算法。