Java多執行緒之執行緒同步方法:synchronized與Lock
一、什麼是執行緒同步?
多個執行緒操作同一個資源,即併發問題: 同一個物件被多個執行緒
同時操作
-
多個執行緒訪問同一個物件,某些執行緒還想修改物件的值,這時候會出現執行緒不安全的問題
-
執行緒同步是一種等待機制,即 多個需要同時訪問此物件的執行緒進入物件的等待池 形成佇列,等待前面執行緒使用完畢,下一個執行緒再使用
-
形成條件: 佇列 + 鎖
-
同一程序的多個執行緒共享同一塊儲存空間,方便之餘也帶來了訪問衝突問題
因此,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制 synchronized,當一個執行緒獲得物件的排他鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖。
- 存在問題:
- 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起
- 多執行緒競爭下,加鎖、釋放鎖會導致較多的上下文切換 和排程延時,引起效能問題
- 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖,會導致優先順序倒置,引起效能問題
- 存在問題:
二、同步方法
- synchronized 關鍵字
-
由於我們可以通過private關鍵字保證資料物件只能被方法訪問,所以我們只需要針對方法提出一套機制,即 synchronized關鍵字,包括兩種用法:
-
- synchronized方法
- synchronized塊
public synchronized void method(int args){ // 同步方法 }
-
-
synchronized 方法控制對 “物件”的訪問,每個物件對應一把鎖
-
synchronized 鎖的是執行緒的共享資源 / 臨界資源
-
缺陷:
- 如果將一個大的方法宣告為synchronized 將會影響效率
- 方法裡面需要修改的內容才需要鎖,鎖的太多反而浪費資源
同步塊
- 同步塊:synchronized(Obj){ }
- Obj稱之為 同步監視器
- Obj可以是任何物件,但是推薦使用共享資源作為同步監視器
- 同步方法中無需指定同步監視器,默認同步監視器為this,也就是物件本身,或者是class
- 同步監視器的執行過程
- 第一個執行緒訪問:鎖定同步監視器,執行其中程式碼
- 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問
- 第一個執行緒訪問完畢,解鎖同步監視器
- 第二個執行緒訪問,發現同步監視器沒有鎖,則鎖定並訪問
- .....
補充知識:
JUC: java.util.concurrent 即併發程式設計
CopyOnWriteArrayList: util.concurrent包下的安全型別集合 可保證執行緒安全,實現執行緒同步的功能!
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); // <> 裡面包含的是 泛型
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
死鎖
- 多個執行緒各自佔有一些資源,並且互相等待其他執行緒佔有的資源才能執行,而導致兩個或多個執行緒都在等待對方釋放資源並停止執行;
- 某一個同步塊同時“擁有兩個以上物件的鎖”時,就可能發生“死鎖”
死鎖避免方法
-
產生死鎖的四個必要條件:
-
- 互斥條件:一個資源每次只能被一個程序使用
- 請求保持條件:一個程序因請求資源而阻塞時,對已經獲得的資源保持不放
- 不剝奪條件:程序已獲得的資源,在未使用完之前,不能強行剝奪
- 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係
-
-
只要想辦法打破其中任意一個或多個條件,即可避免死鎖發生
- Lock(鎖)
-
從JDK5.0開始,Java提供了更強大的執行緒同步機制——顯式定義同步鎖來實現同步
同步鎖使用Lock物件來充當
-
java.util.concurrent.locks Lock介面是控制多個執行緒對共享資源進行訪問的工具,鎖提供了對共享資源的獨佔訪問,每次只有一個執行緒對Lock物件加鎖。執行緒開始訪問共享資源之前應先獲得Lock物件
-
ReentrantLock 類實現了Lock(可重入鎖),擁有與synchronized相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用ReentrantLock,可以顯式加鎖、釋放鎖
synchronized 與 Lock 的對比
- Lock是顯式鎖(需手動開啟和關閉鎖) synchronized 是隱式鎖,出了作用域自動釋放
- Lock只有程式碼塊鎖 synchronized 有程式碼塊鎖 + 方法鎖
- 使用Lock鎖,JVM將花費較少時間來排程執行緒 = > 效能更好 + 可拓展性強(提供更多子類)
- 優先使用順序:
- Lock > 同步程式碼塊(在方法體中,分配了相應資源)> 同步方法(線上程[run()]方法體之外)
class A{
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 將延時放在加鎖之前
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
lock.lock();// 加鎖
// 需要保證執行緒安全的程式碼塊
if (ticketNum > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 拿到了 "+ ticketNum--);
}else{
break;
}
} finally {
lock.unlock(); // 解鎖
}
}
}
}
執行緒協作 重點!
- 生產者(Producer)- 消費者(Consumer)問題
- 執行緒同步問題 生產者消費者共享同一個資源
- 生產者和消費者之間相互依賴,互為條件
- 在生產者-消費者問題中,僅有synchronized是不夠的
- synchronized可阻止併發更新同一個共享資源,實現同步
- synchronized不能用來實現不同執行緒之間的訊息傳遞(通訊)
- 讀(Read)寫(Write)問題
- 理髮師理髮問題
三、執行緒通訊
-
Java提供了幾個方法用於執行緒通訊
- wait() — 執行緒一直等待,知道其他執行緒同志,與sleep不同,wait()會釋放鎖
- wait() — 指定等待的毫秒數
- notify() — 喚醒一個處於等待狀態的執行緒
- notifyAll() — 喚醒同一個物件上所有呼叫了wait() 方法的執行緒,優先級別高的執行緒優先排程
注意:以上方法均為Object類的方法,都只能在同步方法中或者同步程式碼塊中使用,否則會丟擲異常IllegalMonitorStateException
本文來自部落格園,作者:{夕立君},轉載請註明原文連結:https://www.cnblogs.com/liuzhhhao/p/15016079.html