前言
回顧前面:
只有光頭才能變強!
本文章主要講的是Java多執行緒加鎖機制,有兩種:
- Synchronized
- 顯式Lock
不得不嘮叨幾句:
- 在《Java核心技術卷 一》是先講比較難的顯式Lock,而再講的是比較簡單的Synchronized
- 而《Java併發程式設計實戰》在前4章零散地講解了Synchronized,將顯式Lock放到了13章
其實都比較坑,如果能先系統講了Synchronized鎖機制,接著講顯式Lock鎖機制,那就很容易理解了。也不需要跨那麼多章節。
那麼接下來我們就開始吧~
一、synchronized鎖
1.1synchronized鎖是什麼?
synchronized是Java的一個關鍵字,它能夠將程式碼塊(方法)鎖起來
- 它使用起來是非常簡單的,只要在程式碼塊(方法)新增關鍵字synchronized,即可以實現同步的功能~
public synchronized void test() {
// 關注公眾號Java3y
// doSomething
}
synchronized是一種互斥鎖
- 一次只能允許一個執行緒進入被鎖住的程式碼塊
synchronized是一種內建鎖/監視器鎖
- Java中每個物件都有一個內建鎖(監視器,也可以理解成鎖標記),而synchronized就是使用物件的內建鎖(監視器)來將程式碼塊(方法)鎖定的!
1.2synchronized用處是什麼?
- synchronized保證了執行緒的原子性。(被保護的程式碼塊是一次被執行的,沒有任何執行緒會同時訪問)
- synchronized還保證了可見性。(當執行完synchronized之後,修改後的變數對其他的執行緒是可見的)
Java中的synchronized,通過使用內建鎖,來實現對變數的同步操作,進而實現了對變數操作的原子性和其他執行緒對變數的可見性,從而確保了併發情況下的執行緒安全。
1.3synchronized的原理
我們首先來看一段synchronized修飾方法和程式碼塊的程式碼:
public class Main {
//修飾方法
public synchronized void test1(){
}
public void test2(){
// 修飾程式碼塊
synchronized (this){
}
}
}
來反編譯看一下:
同步程式碼塊:
- monitorenter和monitorexit指令實現的
同步方法(在這看不出來需要看JVM底層實現)
- 方法修飾符上的ACC_SYNCHRONIZED實現。
synchronized底層是是通過monitor物件,物件有自己的物件頭,儲存了很多資訊,其中一個資訊標示是被哪個執行緒持有。
具體可參考:
- https://blog.csdn.net/chenssy/article/details/54883355
- https://blog.csdn.net/u012465296/article/details/53022317
1.4synchronized如何使用
synchronized一般我們用來修飾三種東西:
- 修飾普通方法
- 修飾程式碼塊
- 修飾靜態方法
1.4.1修飾普通方法:
用的鎖是Java3y物件(內建鎖)
public class Java3y {
// 修飾普通方法,此時用的鎖是Java3y物件(內建鎖)
public synchronized void test() {
// 關注公眾號Java3y
// doSomething
}
}
1.4.2修飾程式碼塊:
用的鎖是Java3y物件(內建鎖)—>this
public class Java3y {
public void test() {
// 修飾程式碼塊,此時用的鎖是Java3y物件(內建鎖)--->this
synchronized (this){
// 關注公眾號Java3y
// doSomething
}
}
}
當然了,我們使用synchronized修飾程式碼塊時未必使用this,還可以使用其他的物件(隨便一個物件都有一個內建鎖)
所以,我們可以這樣幹:
public class Java3y {
// 使用object作為鎖(任何物件都有對應的鎖標記,object也不例外)
private Object object = new Object();
public void test() {
// 修飾程式碼塊,此時用的鎖是自己建立的鎖Object
synchronized (object){
// 關注公眾號Java3y
// doSomething
}
}
}
上面那種方式(隨便使用一個物件作為鎖)在書上稱之為–>客戶端鎖,這是不建議使用的。
書上想要實現的功能是:給ArrayList新增一個putIfAbsent()
,這需要是執行緒安全的。
假定直接新增synchronized是不可行的
使用客戶端鎖,會將當前的實現與原本的list耦合了:
書上給出的辦法是使用組合的方式(也就是裝飾器模式)
1.4.3修飾靜態方法
獲取到的是類鎖(類的位元組碼檔案物件):Java3y.class
public class Java3y {
// 修飾靜態方法程式碼塊,靜態方法屬於類方法,它屬於這個類,獲取到的鎖是屬於類的鎖(類的位元組碼檔案物件)-->Java3y.class
public synchronized void test() {
// 關注公眾號Java3y
// doSomething
}
}
1.4.4類鎖與物件鎖
synchronized修飾靜態方法獲取的是類鎖(類的位元組碼檔案物件),synchronized修飾普通方法或程式碼塊獲取的是物件鎖。
- 它倆是不衝突的,也就是說:獲取了類鎖的執行緒和獲取了物件鎖的執行緒是不衝突的!
public class SynchoronizedDemo {
//synchronized修飾非靜態方法
public synchronized void function() throws InterruptedException {
for (int i = 0; i <3; i++) {
Thread.sleep(1000);
System.out.println("function running...");
}
}
//synchronized修飾靜態方法
public static synchronized void staticFunction()
throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println("Static function running...");
}
}
public static void main(String[] args) {
final SynchoronizedDemo demo = new SynchoronizedDemo();
// 建立執行緒執行靜態方法
Thread t1 = new Thread(() -> {
try {
staticFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 建立執行緒執行例項方法
Thread t2 = new Thread(() -> {
try {
demo.function();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 啟動
t1.start();
t2.start();
}
}
結果證明:類鎖和物件鎖是不會衝突的!
1.5重入鎖
我們來看下面的程式碼:
public class Widget {
// 鎖住了
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
// 鎖住了
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
- 當執行緒A進入到LoggingWidget的
doSomething()
方法時,此時拿到了LoggingWidget例項物件的鎖。 - 隨後在方法上又呼叫了父類Widget的
doSomething()
方法,它又是被synchronized修飾。 - 那現在我們LoggingWidget例項物件的鎖還沒有釋放,進入父類Widget的
doSomething()
方法還需要一把鎖嗎?
不需要的!
因為鎖的持有者是“執行緒”,而不是“呼叫”。執行緒A已經是有了LoggingWidget例項物件的鎖了,當再需要的時候可以繼續“開鎖”進去的!
這就是內建鎖的可重入性。
1.6釋放鎖的時機
- 當方法(程式碼塊)執行完畢後會自動釋放鎖,不需要做任何的操作。
- 當一個執行緒執行的程式碼出現異常時,其所持有的鎖會自動釋放。
- 不會由於異常導致出現死鎖現象~
二、Lock顯式鎖
2.1Lock顯式鎖簡單介紹
Lock顯式鎖是JDK1.5之後才有的,之前我們都是使用Synchronized鎖來使執行緒安全的~
Lock顯式鎖是一個介面,我們來看看:
隨便翻譯一下他的頂部註釋,看看是幹嘛用的:
可以簡單概括一下:
- Lock方式來獲取鎖支援中斷、超時不獲取、是非阻塞的
- 提高了語義化,哪裡加鎖,哪裡解鎖都得寫出來
- Lock顯式鎖可以給我們帶來很好的靈活性,但同時我們必須手動釋放鎖
- 支援Condition條件物件
- 允許多個讀執行緒同時訪問共享資源
2.2synchronized鎖和Lock鎖使用哪個
前面說了,Lock顯式鎖給我們的程式帶來了很多的靈活性,很多特性都是Synchronized鎖沒有的。那Synchronized鎖有沒有存在的必要??
必須是有的!!Lock鎖在剛出來的時候很多效能方面都比Synchronized鎖要好,但是從JDK1.6開始Synchronized鎖就做了各種的優化(畢竟親兒子,牛逼)
- 優化操作:適應自旋鎖,鎖消除,鎖粗化,輕量級鎖,偏向鎖。
- 詳情可參考:https://blog.csdn.net/chenssy/article/details/54883355
所以,到現在Lock鎖和Synchronized鎖的效能其實差別不是很大!而Synchronized鎖用起來又特別簡單。Lock鎖還得顧忌到它的特性,要手動釋放鎖才行(如果忘了釋放,這就是一個隱患)
所以說,我們絕大部分時候還是會使用Synchronized鎖,用到了Lock鎖提及的特性,帶來的靈活性才會考慮使用Lock顯式鎖~
2.3公平鎖
公平鎖理解起來非常簡單:
- 執行緒將按照它們發出請求的順序來獲取鎖
非公平鎖就是:
- 執行緒發出請求的時可以“插隊”獲取鎖
Lock和synchronize都是預設使用非公平鎖的。如果不是必要的情況下,不要使用公平鎖
- 公平鎖會來帶一些效能的消耗的
四、最後
本文講了synchronized內建鎖和簡單描述了一下Lock顯式鎖,總得來說:
- synchronized好用,簡單,效能不差
- 沒有使用到Lock顯式鎖的特性就不要使用Lock鎖了。
Lock鎖這裡只是介紹了一些些,明天會詳解它的相關子類和需要注意的地方,敬請期待~
之前在學習作業系統的時候根據《計算機作業系統-湯小丹》這本書也做了一點點筆記,都是比較淺顯的知識點。或許對大家有幫助
參考資料:
- 《Java核心技術卷一》
- 《Java併發程式設計實戰》
- 《計算機作業系統-湯小丹》
- https://blog.csdn.net/panweiwei1994/article/details/78483167
- http://www.cnblogs.com/dolphin0520/category/602384.html
- https://blog.csdn.net/chenssy/article/category/3145247
- https://blog.csdn.net/u012465296/article/details/53022317
- https://www.cnblogs.com/wxd0108/p/5479442.html
如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關注微信公眾號:Java3y。為了大家方便,剛新建了一下qq群:742919422,大家也可以去交流交流。謝謝支援了!希望能多介紹給其他有需要的朋友
文章的目錄導航: