前言
并發編程技術在Java中屬于重要知識點,對于以下內容你有了解多少?
進程、線程、協程關系概述
進程:本質上是一個獨立執行的程序,進程是操作系統進行資源分配和調度的基本概念,操作系統進行資源分配和調度的一個獨立單位。
?
線程:操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一個進程中可以并發多個線程,每條線程執行不同的任務,切換受系統控制。
?
協程:又稱為微線程,是一種用戶態的輕量級線程,協程不像線程和進程需要進行系統內核上的上下文切換,協程的上下文切換是由用戶自己決定的,有自己的上下文,所以說是輕量級的線程,也稱之為用戶級別的線程,一個線程可以有多個協程,線程與進程都是同步機制,而協程則是異步。Java的原生語法中并沒有實現協程,目前python、Lua和GO等語言支持。
?
關系:一個進程可以有多個線程,它允許計算機同時運行兩個或多個程序。線程是進程的最小執行單位,CPU的調度切換的是進程和線程,進程和線程多了之后調度會消耗大量的CPU,CPU上真正運行的是線程,線程可以對應多個協程。
協程對于多線程的優缺點
優點:
- 非常快速的上下文切換,不用系統內核的上下文切換,減小開銷
- 單線程即可實現高并發,單核CPU可以支持上萬的協程
- 由于只有一個線程,也不存在同時寫變量的沖突,在協程中控制共享資源不需要加鎖
缺點:
- 協程無法利用多核資源,本質也是個單線程
- 協程需要和進程配合才能運行在多CPU上
- 目前Java沒成熟的第三方庫,存在風險
- 調試debug存在難度,不利于發現問題
并發和并行的區別
并發 (concurrency):一臺處理器上同時處理多個任務,這個同時實際上是交替處理多個任務,程序中可以同時擁有兩個或者多個線程,當有多個線程在操作時,如果系統只有一個CPU,則它根本不可能真正同時進行一個以上的線程,它只能把CPU運行時間劃分成若干個時間段,再將時間段分配給各個線程執行。
?
并行(parallellism) :多個CPU上同時處理多個任務,一個CPU執行一個進程時,另一個CPU可以執行另一個進程,兩個進程互不搶占CPU資源,可以同時進行。
并發指在一段時間內宏觀上去處理多個任務。 并行指同一個時刻,多個任務確實真的同時運行。
Java里實現多線程的幾種方式
1.繼承Thread類
繼承Thread類,重寫里面run方法,創建實例,執行start方法。
優點:代碼編寫最簡單直接操作
缺點:無返回值,繼承一個類后,沒法繼承其他的類,拓展性差
public class ThreadDemo1 extends Thread {
@Override
public void run() {
System.out.println("繼承Thread實現多線程、名稱:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
?
ThreadDemo1 threadDemo1 = new ThreadDemo1();
threadDemo1.setName("demo1");
threadDemo1.start();
System.out.println("主線程名稱:"+Thread.currentThread().getName());
?
}
2.實現Runnable接口
自定義類實現Runnable接口,實現里面run方法,創建Thread類,使用Runnable接口的實現對象作為參數傳遞給Thread對象,調用start方法。
?
優點:線程類可以實現多個幾接口,可以再繼承一個類
缺點:無返回值,不能直接啟動,需要通過構造一個Thread實例傳遞進去啟動
public class ThreadDemo2 implements Runnable {
?
@Override
public void run() {
System.out.println("通過Runnable實現多線程、名稱:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadDemo2 threadDemo2 = new ThreadDemo2();
Thread thread = new Thread(threadDemo2);
thread.setName("demo2");
thread.start();
System.out.println("主線程名稱:"+Thread.currentThread().getName());
}
JDK8之后采用lambda表達式
public static void main(String[] args) {
Thread thread = new Thread(()->{
System.out.println("通過Runnable實現多線程、名稱:"+Thread.currentThread().getName());
});
thread.setName("demo2");
thread.start();
System.out.println("主線程名稱:"+Thread.currentThread().getName());
}
3.通過Callable和FutureTask方式
創建callable接口的實現類,并實現call方法,結合FutureTask類包裝callable對象,實現多線程。
優點:有返回值,拓展性也高
缺點:jdk5以后才支持,需要重寫call方法,結合多個類比如FutureTask和Thread類
public class MyTask implements Callable<Object> {
@Override
public Object call() throws Exception {
?
System.out.println("通過Callable實現多線程、名稱:"+Thread.currentThread().getName());
?
return "這是返回值";
}
}
public static void main(String[] args) {
?
MyTask myTask = new MyTask();
FutureTask<Object> futureTask = new FutureTask<>(myTask);
?
//FutureTask繼承了Runnable,可以放在Thread中啟動執行
Thread thread = new Thread(futureTask);
thread.setName("demo3");
thread.start();
System.out.println("主線程名稱:"+Thread.currentThread().getName());
?
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
//阻塞等待中被中斷則拋出
e.printStackTrace();
} catch (ExecutionException e) {
//執行過程發送異常被拋出
e.printStackTrace();
}
}
采用lambda表達式
public static void main(String[] args) {
?
FutureTask<Object> futureTask = new FutureTask<>(()->{
System.out.println("通過Callable實現多線程、名稱:"+Thread.currentThread().getName());
return "這是返回值";
});
//FutureTask繼承了Runnable,可以放在Thread中啟動執行
Thread thread = new Thread(futureTask);
thread.setName("demo3");
thread.start();
System.out.println("主線程名稱:"+Thread.currentThread().getName());
?
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
//阻塞等待中被中斷則拋出
e.printStackTrace();
} catch (ExecutionException e) {
//執行過程發送異常被拋出
e.printStackTrace();
}
?
?
}
4.通過線程池創建線程
自定義Runnable接口,實現run方法,創建線程池,調用執行方法并傳入對象。
優點:安全高性能,復用線程
缺點: jdk5后才支持,需要結合Runnable進行使用
public class ThreadDemo4 implements Runnable {
?
@Override
public void run() {
System.out.println("通過線程池+runnable實現多線程,名稱:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
?
for(int i=0;i<10;i++){
executorService.execute(new ThreadDemo4());
}
?
System.out.println("主線程名稱:"+Thread.currentThread().getName());
?
//關閉線程池
executorService.shutdown();
}
- 一般常用的Runnable 和 第四種線程池+Runnable,簡單方便擴展,和高性能 (池化的思想)
Java線程常見的基本狀態
JDK的線程狀態分6種,JVM里面9種。
常見的5種狀態
創建(NEW):生成線程對象,但是并沒有調用該對象start()。
?
就緒(Runnable):當調用線程對象的start()方法,線程就進入就緒狀態,但是此刻線程調度還沒把該線程設置為當前線程,就是沒獲得CPU使用權。如果線程運行后,從等待或者睡眠中回來之后,也會進入就緒狀態。
運行(Running):程序將處于就緒狀態的線程設置為當前線程,即獲得CPU使用權,這個時候線程進入運行狀態,開始運行run里面的邏輯。
?
阻塞(Blocked)
等待阻塞:進入該狀態的線程需要等待其他線程作出一定動作(通知或中斷),這種狀態的話CPU不會分配過來,他們需要被喚醒,可能也會無限等待下去。比如調用wait(狀態就會變成WAITING狀態),也可能通過調用sleep(狀態就會變成TIMED_WAITING), join或者發出IO請求,阻塞結束后線程重新進入就緒狀態。
同步阻塞:線程在獲取synchronized同步鎖失敗,即鎖被其他線程占用,它就會進入同步阻塞狀態。
備注:相關資料會用細分下面的狀態
等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
超時等待(TIMED_WAITING):該狀態不同于WAITING,它可以在指定的時間后自行返回。
死亡(TERMINATED):一個線程run方法執行結束,該線程就死亡了,不能進入就緒狀態。
多線程開發常用方法
sleep
屬于線程Thread的方法;
讓線程暫緩執行,等待預計時間之后再恢復;
交出CPU使用權,不會釋放鎖;
進入阻塞狀態TIME_WAITGING,睡眠結束變為就緒Runnable;
yield
屬于線程Thread的方法;
暫停當前線程的對象,去執行其他線程;
交出CPU使用權,不會釋放鎖,和sleep類似;
作用:讓相同優先級的線程輪流執行,但是不保證一定輪流;
注意:不會讓線程進入阻塞狀態,直接變為就緒Runnable,只需要重新獲得CPU使用權;
join
屬于線程Thread的方法;
在主線程上運行調用該方法,會讓主線程休眠,不會釋放已經持有的對象鎖;
讓調用join方法的線程先執行完畢,再執行其他線程;
wait
屬于Object的方法;
當前線程調用對象的wait方法,會釋放鎖,進入線程的等待隊列;
需要依靠notify或者notifyAll喚醒,或者wait(timeout)時間自動喚醒;
notify
屬于Object的方法;
喚醒在對象監視器上等待的單個線程,選擇是任意的;
notifyAll
屬于Object的方法;
喚醒在對象監視器上等待的全部線程;
線程的狀態轉換圖
Java中保證線程安全的方法
- 加鎖,比如synchronize/ReentrantLock
- 使用volatile聲明變量,輕量級同步,不能保證原子性
- 使用線程安全類,原子類AtomicXXX,并發容器,同步CopyOnWriteArrayList/ConcurrentHashMap等
- ThreadLocal本地私有變量/信號量Semaphore等
解析volatile關鍵字
volatile是輕量級的synchronized,保證了共享變量的可見性,被volatile關鍵字修飾的變量,如果值發生了變化,其他線程立刻可見,避免出現臟讀現象。為什么會出現臟讀?JAVA內存模型簡稱JMM,JMM規定所有的變量存在在主內存,每個線程有自己的工作內存,線程對變量的操作都在工作內存中進行,不能直接對主內存就行操作,使用volatile修飾變量,每次讀取前必須從主內存屬性獲取最新的值,每次寫入需要立刻寫到主內存中。volatile關鍵字修修飾的變量隨時看到的自己的最新值,假如線程1對變量v進行修改,那么線程2是可以馬上看見的。
?
volatile:保證可見性,但是不能保證原子性
synchronized:保證可見性,也保證原子性
?什么是指令重排
指令重排序分兩類:編譯器重排序和運行時重排序
?
JVM在編譯Java代碼或者CPU執行JVM字節碼時,對現有的指令進行重新排序,主要目的是優化運行效率(不改變程序結果的前提)
?舉例:
int a = 3 //第一步 1
int b = 4 //第二步 2
int c =5 //第三步 3
int h = abc //第四步 4
?
定義順序 1,2,3,4
計算順序 1,3,2,4 和 2,1,3,4 結果都是一樣
什么是happens-before以及為什么需要happens-before
happens-before:A happens-before B就是A先行發生于B(這種說法不是很準確),定義為hb(A, B)。在Java內存模型中,happens-before的意思是前一個操作的結果可以被后續操作獲取。JVM會對代碼進行編譯優化,會出現指令重排序情況,為了避免編譯優化對并發編程安全性的影響,需要happens-before規則定義一些禁止編譯優化的場景,保證并發編程的正確性。
happens-before八大規則
1.程序次序規則:在一個線程內一段代碼的執行結果是有序的。就是還會指令重排,但是隨便它怎么排,結果是按照我們代碼的順序生成的不會變。
2.管程鎖定規則:就是無論是在單線程環境還是多線程環境,對于同一個鎖來說,一個線程對這個鎖解鎖之后,另一個線程獲取了這個鎖都能看到前一個線程的操作結果!(管程是一種通用的同步原語,synchronized就是管程的實現)
3.volatile變量規則:就是如果一個線程先去寫一個volatile變量,然后一個線程去讀這個變量,那么這個寫操作的結果一定對讀的這個線程可見。
4.線程啟動規則:在主線程A執行過程中,啟動子線程B,那么線程A在啟動子線程B之前對共享變量的修改結果對線程B可見。
5.線程終止規則:在主線程A執行過程中,子線程B終止,那么線程B在終止之前對共享變量的修改結果在線程A中可見。也稱線程join()規則。
6.線程中斷規則:對線程interrupt()方法的調用先行發生于被中斷線程代碼檢測到中斷事件的發生,可以通過Thread.interrupted()檢測到是否發生中斷。
7.傳遞性規則:這個簡單的,就是happens-before原則具有傳遞性,即hb(A, B) , hb(B, C),那么hb(A, C)。
8.對象終結規則:這個也簡單的,就是一個對象的初始化的完成,也就是構造函數執行的結束一定 happens-before它的finalize()方法。
并發編程三要素
原子性:一個不可再被分割的顆粒,原子性指的是一個或多個操作要么全部執行成功要么全部執行失敗,期間不能被中斷,也不存在上下文切換,線程切換會帶來原子性的問題
int num = 1; // 原子操作
num++; // 非原子操作,從主內存讀取num到線程工作內存,進行 +1,再把num寫到主內存, 除非用原子類,即java.util.concurrent.atomic里的原子變量類
?
解決辦法是可以用synchronized 或 Lock(比如ReentrantLock) 來把這個多步操作“變成”原子操作
public class Test {
private int num = 0;
//使用lock,每個對象都是有鎖,只有獲得這個鎖才可以進行對應的操作
Lock lock = new ReentrantLock();
public void add1(){
lock.lock();
try {
num++;
}finally {
lock.unlock();
}
}
//使用synchronized,和上述是一個操作,這個是保證方法被鎖住而已,上述的是代碼塊被鎖住
public synchronized void add2(){
num++;
}
}
解決核心思想:把一個方法或者代碼塊看做一個整體,保證是一個不可分割的整體
?
有序性: 程序執行的順序按照代碼的先后順序執行,因為處理器可能會對指令進行重排序
JVM在編譯java代碼或者CPU執行JVM字節碼時,對現有的指令進行重新排序,主要目的是優化運行效率(不改變程序結果的前提)
int a = 3 //第一步 1
int b = 4 //第二步 2
int c =5 //第三步 3
int h = a*b*c //第四步 4
上面的例子 執行順序1,2,3,4 和 2,1,3,4 結果都是一樣,指令重排序可以提高執行效率,但是多線程上可能會影響結果
?
假如下面的場景,正常是順序處理
//線程1
before();//處理初始化工作,處理完成后才可以正式運行下面的run方法
flag = true; //標記資源處理好了,如果資源沒處理好,此時程序就可能出現問題
//線程2
while(flag){
run(); //核心業務代碼
}
指令重排序后,導致順序換了,程序出現問題,且難排查
//線程1
flag = true; //標記資源處理好了,如果資源沒處理好,此時程序就可能出現問題
//線程2
while(flag){
run(); //核心業務代碼
}
before();//處理初始化工作,處理完成后才可以正式運行下面的run方法
可見性: 一個線程A對共享變量的修改,另一個線程B能夠立刻看到
// 線程 A 執行
int num = 0;
// 線程 A 執行
num++;
// 線程 B 執行
System.out.print("num的值:" + num);
線程A執行 i++ 后再執行線程 B,線程 B可能有2個結果,可能是0和1。
?
因為 i++ 在線程A中執行運算,并沒有立刻更新到主內存當中,而線程B就去主內存當中讀取并打印,此時打印的就是0;也可能線程A執行完成更新到主內存了,線程B的值是1。所以需要保證線程的可見性,synchronized、lock和volatile能夠保證線程可見性。
常見的進程間調度算法
先來先服務調度算法:
按照作業/進程到達的先后順序進行調度 ,即:優先考慮在系統中等待時間最長的作業,排在長進程后的短進程的等待時間長,不利于短作業/進程
?
短作業優先調度算法:
短進程/作業(要求服務時間最短)在實際情況中占有很大比例,為了使得它們優先執行,對長作業不友好
?
高響應比優先調度算法:
在每次調度時,先計算各個作業的優先權:優先權=響應比=(等待時間+要求服務時間)/要求服務時間,因為等待時間與服務時間之和就是系統對該作業的響應時間,所以 優先權=響應比=響應時間/要求服務時間,選擇優先權高的進行服務需要計算優先權信息,增加了系統的開銷
時間片輪轉調度算法:
輪流的為各個進程服務,讓每個進程在一定時間間隔內都可以得到響應,由于高頻率的進程切換,會增加了開銷,且不區分任務的緊急程度
?
優先級調度算法:
根據任務的緊急程度進行調度,高優先級的先處理,低優先級的慢處理,如果高優先級任務很多且持續產生,那低優先級的就可能很慢才被處理
常見的線程間調度算法
線程調度是指系統為線程分配CPU使用權的過程,主要分兩種:
?
協同式線程調度(分時調度模式):線程執行時間由線程本身來控制,線程把自己的工作執行完之后,要主動通知系統切換到另外一個線程上。最大好處是實現簡單,且切換操作對線程自己是可知的,沒啥線程同步問題。壞處是線程執行時間不可控制,如果一個線程有問題,可能一直阻塞在那里。
?
搶占式線程調度:每個線程將由系統來分配執行時間,線程的切換不由線程本身來決定(Java中,Thread.yield()可以讓出執行時間,但無法獲取執行時間)。線程執行時間系統可控,也不會由一個線程導致整個進程阻塞。
?
?Java線程調度就是搶占式調度,優先讓可運行池中優先級高的線程占用CPU,如果可運行池中的線程優先級相同,那就隨機選擇一個線程。所以我們如果希望某些線程多分配一些時間,給一些線程少分配一些時間,可以通過設置線程優先級來完成。
JAVA的線程的優先級,以1到10的整數指定。當多個線程可以運行時,JVM一般會運行最高優先級的線程(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)。在兩線程同時處于就緒runnable狀態時,優先級越高的線程越容易被系統選擇執行,但是優先級并不是100%可以獲得,只不過是機會更大而已。
Java多線程里面常用的鎖
悲觀鎖:當線程去操作數據的時候,總認為別的線程會去修改數據,所以它每次拿數據的時候都會上鎖,別的線程去拿數據的時候就會阻塞,比如synchronized
樂觀鎖:每次去拿數據的時候都認為別人不會修改,更新的時候會判斷是別人是否回去更新數據,通過版本來判斷,如果數據被修改了就拒絕更新,比如CAS是樂觀鎖,但嚴格來說并不是鎖,是通過原子性來保證數據的同步,比如說數據庫的樂觀鎖,通過版本控制來實現,CAS不會保證線程同步,樂觀的認為在數據更新期間沒有其他線程影響
小結:悲觀鎖適合寫操作多的場景,樂觀鎖適合讀操作多的場景,樂觀鎖的吞吐量會比悲觀鎖多。
?
公平鎖:指多個線程按照申請鎖的順序來獲取鎖,簡單來說,一個線程組里,能保證每個線程都能拿到鎖,比如ReentrantLock(底層是同步隊列FIFO:First Input First Output來實現)
非公平鎖:獲取鎖的方式是隨機獲取的,保證不了每個線程都能拿到鎖,也就是存在有線程餓死,一直拿不到鎖,比如synchronized、ReentrantLock
小結:非公平鎖性能高于公平鎖,更能重復利用CPU的時間。
?
可重入鎖:也叫遞歸鎖,在外層使用鎖之后,在內層仍然可以使用,并且不發生死鎖
不可重入鎖:若當前線程執行某個方法已經獲取了該鎖,那么在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞
小結:可重入鎖能一定程度的避免死鎖 ,synchronized、ReentrantLock 是重入鎖。
?
自旋鎖:一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那么該線程將循環等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出循環,任何時刻最多只能有一個執行單元獲得鎖
小結:自旋鎖不會發生線程狀態的切換,一直處于用戶態,減少了線程上下文切換的消耗,缺點是循環會消耗CPU。常見的自旋鎖:TicketLock,CLHLock,MSCLock。
共享鎖:也叫S鎖/讀鎖,能查看但無法修改和刪除的一種數據鎖,加鎖后其它用戶可以并發讀取、查詢數據,但不能修改,增加,刪除數據,該鎖可被多個線程所持有,用于資源數據共享。
?
互斥鎖:也叫X鎖/排它鎖/寫鎖/獨占鎖/獨享鎖/ 該鎖每一次只能被一個線程所持有,加鎖后任何試圖再次加鎖的線程會被阻塞,直到當前線程解鎖。例子:如果 線程A 對 data1 加上排他鎖后,則其他線程不能再對 data1 加任何類型的鎖,獲得互斥鎖的線程即能讀數據又能修改數據。
?
死鎖:兩個或兩個以上的線程在執行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法讓程序進行下去。
?
下面三種是JVM為了提高鎖的獲取與釋放效率而做的優化,針對Synchronized的鎖升級,鎖的狀態是通過對象監視器在對象頭中的字段來表明,是不可逆的過程。
偏向鎖:一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖,獲取鎖的代價更低。
輕量級鎖:當鎖是偏向鎖的時候,被其他線程訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,但不會阻塞,且性能會高點。
重量級鎖:當鎖為輕量級鎖的時候,其他線程雖然是自旋,但自旋不會一直循環下去,當自旋一定次數的時候且還沒有獲取到鎖,就會進入阻塞,該鎖升級為重量級鎖,重量級鎖會讓其他申請的線程進入阻塞,性能也會降低。
?
分段鎖、行鎖、表鎖
編寫多線程死鎖的例子
死鎖:線程在獲得了鎖A并且沒有釋放的情況下去申請鎖B,這時另一個線程已經獲得了鎖B,在釋放鎖B之前又要先獲得鎖A,因此閉環發生,陷入死鎖循環。
public class DeadLockDemo {
?
private static String locka = "locka";
?
private static String lockb = "lockb";
?
public void methodA(){
?
synchronized (locka){
System.out.println("我是A方法中獲得了鎖A "+Thread.currentThread().getName() );
?
//讓出CPU執行權,不釋放鎖
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
?
synchronized(lockb){
System.out.println("我是A方法中獲得了鎖B "+Thread.currentThread().getName() );
}
}
}
?
public void methodB(){
synchronized (lockb){
System.out.println("我是B方法中獲得了鎖B "+Thread.currentThread().getName() );
?
//讓出CPU執行權,不釋放鎖
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
?
synchronized(locka){
System.out.println("我是B方法中獲得了鎖A "+Thread.currentThread().getName() );
}
}
?
}
?
public static void main(String [] args){
?
System.out.println("主線程運行開始運行:"+Thread.currentThread().getName());
?
DeadLockDemo deadLockDemo = new DeadLockDemo();
?
new Thread(()->{
deadLockDemo.methodA();
}).start();
?
new Thread(()->{
deadLockDemo.methodB();
}).start();
?
System.out.println("主線程運行結束:"+Thread.currentThread().getName());
?
}
?
}
對于上面的例子如何解決死鎖,常見的解決辦法有兩種:
- 調整申請鎖的范圍
- 調整申請鎖的順序
public class FixDeadLockDemo {
?
private static String locka = "locka";
?
private static String lockb = "lockb";
?
public void methodA(){
?
synchronized (locka){
System.out.println("我是A方法中獲得了鎖A "+Thread.currentThread().getName() );
?
//讓出CPU執行權,不釋放鎖
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
?
}
?
synchronized(lockb){
System.out.println("我是A方法中獲得了鎖B "+Thread.currentThread().getName() );
}
}
?
?
public void methodB(){
synchronized (lockb){
System.out.println("我是B方法中獲得了鎖B "+Thread.currentThread().getName() );
?
//讓出CPU執行權,不釋放鎖
try {
Thread.sleep(2000);
?
} catch (InterruptedException e) {
e.printStackTrace();
}
?
}
?
synchronized(locka){
System.out.println("我是B方法中獲得了鎖A "+Thread.currentThread().getName() );
}
}
?
?
public static void main(String [] args){
?
System.out.println("主線程運行開始運行:"+Thread.currentThread().getName());
?
FixDeadLockDemo deadLockDemo = new FixDeadLockDemo();
?
for(int i=0; i<10;i++){
new Thread(()->{
deadLockDemo.methodA();
}).start();
?
new Thread(()->{
deadLockDemo.methodB();
}).start();
}
?
System.out.println("主線程運行結束:"+Thread.currentThread().getName());
?
}
?
}
死鎖的4個必要條件:
- 互斥條件:進程對所分配到的資源不允許其他進程進行訪問,若其他進程訪問該資源,只能等待,直至占有該資源的進程使用完成后釋放該資源
- 請求和保持條件:進程獲得一定的資源之后,又對其他資源發出請求,但是該資源可能被其他進程占有,此事請求阻塞,但又對自己獲得的資源保持不放
- 不可剝奪條件:是指進程已獲得的資源,在未完成使用之前,不可被剝奪,只能在使用完后自己釋放
- 環路等待條件:是指進程發生死鎖后,若干進程之間形成一種頭尾相接的循環等待資源關系
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之 一不滿足,就不會發生死鎖。
設計一個簡單的不可重入鎖例子
不可重入鎖:若當前線程執行某個方法已經獲取了該鎖,那么在其他方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。
private void methodA(){
//獲取鎖 TODO
methodB();
}
?
private void methodB(){
//獲取鎖 TODO
//其他操作
}
/**
* 不可重入鎖 簡單例子
*
* 不可重入鎖:若當前線程執行某個方法已經獲取了該鎖,那么在其他方法中嘗試再次獲取鎖時,就會獲取不到被阻塞
*/
public class UnreentrantLock {
?
private boolean isLocked = false;
?
public synchronized void lock() throws InterruptedException {
?
System.out.println("進入lock加鎖 "+Thread.currentThread().getName());
?
//判斷是否已經被鎖,如果被鎖則當前請求的線程進行等待
while (isLocked){
System.out.println("進入wait等待 "+Thread.currentThread().getName());
wait();
}
//進行加鎖
isLocked = true;
}
public synchronized void unlock(){
System.out.println("進入unlock解鎖 "+Thread.currentThread().getName());
isLocked = false;
//喚醒對象鎖池里面的一個線程
notify();
}
}
?
?
?
public class Main {
private UnreentrantLock unreentrantLock = new UnreentrantLock();
//加鎖建議在try里面,解鎖建議在finally
public void methodA(){
try {
unreentrantLock.lock();
System.out.println("methodA方法被調用");
methodB();
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
unreentrantLock.unlock();
}
}
?
public void methodB(){
try {
unreentrantLock.lock();
System.out.println("methodB方法被調用");
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
unreentrantLock.unlock();
}
}
public static void main(String [] args){
//演示的是同個線程
new Main().methodA();
}
}
?
//同一個線程,重復獲取鎖失敗,形成死鎖,這個就是不可重入鎖
設計一個簡單的可重入鎖例子
可重入鎖:也叫遞歸鎖,在外層使用鎖之后,在內層仍然可以使用,并且不發生死鎖
/**
* 可重入鎖 簡單例子
*
* 可重入鎖:也叫遞歸鎖,在外層使用鎖之后,在內層仍然可以使用,并且不發生死鎖
*/
public class ReentrantLock {
?
private boolean isLocked = false;
?
//用于記錄是不是重入的線程
private Thread lockedOwner = null;
?
//累計加鎖次數,加鎖一次累加1,解鎖一次減少1
private int lockedCount = 0;
?
public synchronized void lock() throws InterruptedException {
?
System.out.println("進入lock加鎖 "+Thread.currentThread().getName());
?
Thread thread = Thread.currentThread();
?
//判斷是否是同個線程獲取鎖, 引用地址的比較
while (isLocked && lockedOwner != thread ){
System.out.println("進入wait等待 "+Thread.currentThread().getName());
System.out.println("當前鎖狀態 isLocked = "+isLocked);
System.out.println("當前count數量 lockedCount = "+lockedCount);
wait();
}
?
//進行加鎖
isLocked = true;
lockedOwner = thread;
lockedCount++;
}
public synchronized void unlock(){
System.out.println("進入unlock解鎖 "+Thread.currentThread().getName());
?
Thread thread = Thread.currentThread();
?
//線程A加的鎖,只能由線程A解鎖,其他線程B不能解鎖
if(thread == this.lockedOwner){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
lockedOwner = null;
//喚醒對象鎖池里面的一個線程
notify();
}
}
}
}
public class Main {
//private UnreentrantLock unreentrantLock = new UnreentrantLock();
private ReentrantLock reentrantLock = new ReentrantLock();
?
//加鎖建議在try里面,解鎖建議在finally
public void methodA(){
?
try {
reentrantLock.lock();
System.out.println("methodA方法被調用");
methodB();
?
}catch (InterruptedException e){
e.fillInStackTrace();
?
} finally {
reentrantLock.unlock();
}
?
}
?
public void methodB(){
?
try {
reentrantLock.lock();
System.out.println("methodB方法被調用");
?
}catch (InterruptedException e){
e.fillInStackTrace();
?
} finally {
reentrantLock.unlock();
}
}
?
public static void main(String [] args){
for(int i=0 ;i<10;i++){
//演示的是同個線程
new Main().methodA();
}
}
}
歡迎補充!!!
最后
感謝你看到這里,看完有什么的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!