JAVA學習筆記(併發程式設計 - 玖)- 多執行緒併發拓展
文章目錄
死鎖
概念
死鎖是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。
產生條件
雖然程序在執行過程中,可能發生死鎖,但死鎖的發生也必須具備一定的條件,死鎖的發生必須具備以下四個必要條件。
- 互斥條件:指程序對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個程序佔用。如果此時還有其它程序請求資源,則請求者只能等待,直至佔有資源的程序用畢釋放。
- 請求和保持條件:指程序已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程序佔有,此時請求程序阻塞,但又對自己已獲得的其它資源保持不放。
- 不剝奪條件:指程序已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
- 環路等待條件:指在發生死鎖時,必然存在一個程序——資源的環形鏈,即程序集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。
例子
package com.mmall.concurrency.example.deadLock;
import lombok. extern.slf4j.Slf4j;
/**
* 一個簡單的死鎖類
* 當DeadLock類的物件flag==1時(td1),先鎖定o1,睡眠500毫秒
* 而td1在睡眠的時候另一個flag==0的物件(td2)執行緒啟動,先鎖定o2,睡眠500毫秒
* td1睡眠結束後需要鎖定o2才能繼續執行,而此時o2已被td2鎖定;
* td2睡眠結束後需要鎖定o1才能繼續執行,而此時o1已被td1鎖定;
* td1、td2相互等待,都需要得到對方鎖定的資源才能繼續執行,從而死鎖。
*/
@Slf4j
public class DeadLock implements Runnable {
public int flag = 1;
//靜態物件是類的所有物件共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
log.info("flag:{}", flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
log.info("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
log.info("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都處於可執行狀態,但JVM執行緒排程先執行哪個執行緒是不確定的。
//td2的run()可能在td1的run()之前執行
new Thread(td1).start();
new Thread(td2).start();
}
}
/*
14:43:34.169 [Thread-1] INFO com.mmall.concurrency.example.deadLock.DeadLock - flag:0
14:43:34.169 [Thread-0] INFO com.mmall.concurrency.example.deadLock.DeadLock - flag:1
*/
併發最佳實踐
-
使用本地變數
應該儘量使用本地變數,而不是建立一個類或者例項的變數。 -
使用不可變類
String、Integer等。不可變類可以降低程式碼中的需要的同步數量 -
最小化鎖的作用域範圍:S=1/(1-a+a/n)
a:平行計算部分所佔比例
n:並行處理結點個數
S:加速比
當1-a等於0時,沒有序列只有並行,最大加速比 S=n
當a=0時,只有序列沒有並行,最小加速比 S = 1
當n→∞時,極限加速比 s→ 1/(1-a)
例如,若序列程式碼佔整個程式碼的25%,則並行處理的總體效能不可能超過4。該公式稱為:“阿姆達爾定律"或"安達爾定理”。 -
使用執行緒池的Executor,而不是直接new Thread 執行
建立一個執行緒的代價是昂貴的,如果要建立一個可伸縮的Java應用,那麼你需要使用執行緒池。 -
寧可使用同步也不要使用執行緒的wait和notify
從Java1.5以後,增加了許多同步工具,如:CountDownLatch、CyclicBarrier、Semaphore等,應該優先使用這些同步工具。 -
使用BlockingQueue實現生產-消費模式
阻塞佇列不僅可以處理單個生產、單個消費,也可以處理多個生產和消費。 -
使用併發集合而不是加了鎖的同步集合
Java提供了下面幾種併發集合框架:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentLinkedQueue 、ConcurrentLinkedDeque
-
使用Semaphone建立有界的訪問
為了建立穩定可靠的系統,對於資料庫、檔案系統和socket等資源必須要做有機的訪問,Semaphone可以限制這些資源開銷的選擇,Semaphone可以以最低的代價阻塞執行緒等待,可以通過Semaphone來控制同時訪問指定資源的執行緒數。 -
寧可使用同步程式碼塊,也不實用同步的方法
主要針對synchronized關鍵字。使用synchronized關鍵字同步程式碼塊只會鎖定一個物件,而不會將整個方法鎖定。如果更改共同的變數或類的欄位,首先應該選擇的是原子型變數,然後使用volatile。如果需要互斥鎖,可以考慮使用ReentrantLock。 -
避免使用靜態變數
靜態變數在併發執行環境下會製造很多問題,如果必須使用靜態變數,那麼優先是它成為final變數,如果用來儲存集合collection,那麼可以考慮使用只讀集合,否則一定要做特別多的同步處理和併發處理操作。
Spring與執行緒安全
Spring作為一個IOC容器幫助我們管理了許多bean,但Spring並沒有保證它們的執行緒安全,而是將這個任務交給了開發者,需要開發者來編寫執行緒安全的程式碼。
- Spring bean : singleton , prototype
Spring在建立Bean時需要為其設定作用域,以singleton為例,它是Spring的預設作用域即單例模式,它的生命週期和Spring容器是一致的,只在第一次注入時會被建立,另一個prototype,每次注入都會建立。 - 無狀態的物件
無狀態的物件很適合以singleton,它不用擔心多個執行緒的操作而導致自身狀態被破壞,因此可以說每個無狀態的物件都是執行緒安全的。我們實際使用的DAO、DTO、Entity等等各種業務物件就是無狀態物件,它們只是負責執行某些操作或者傳遞資料,其自身不攜帶狀態。
可以通過將bean的作用域都設定為prototype來保證執行緒安全嗎?理論上可以,但是這種頻繁建立物件的方式會極大地影響應用的效能。