一、多執行緒之間的通信(Java版本)
1、多執行緒概念介紹
多執行緒概念
-
在我們的程式層面來說,
多執行緒通常是在每個行程中執行的,相應的附和我們常說的執行緒與行程之間的關系,執行緒與行程的關系:執行緒可以說是行程的兒子,一個行程可以有多個執行緒,但是對于執行緒來說,只屬于一個行程,再說說行程,每個行程的有一個主執行緒作為入口,也有自己的唯一標識PID,它的PID也就是這個主執行緒的執行緒ID, -
對于我們的計算機硬體來說,執行緒是
行程中的一部分,也是行程的的實際運作單位,它也是作業系統中的最小運算調度單位,多執行緒可以提高CPU的處理速度,當然除了單核CPU,因為單核心CPU同一時間只能處理一個執行緒,在多執行緒環境下,對于單核CP來說,并不能提高回應速度,而且還會因為頻繁切換執行緒背景關系導致性能降低,多核心CPU具有同時并行執行執行緒的能力,因此我們需要注意使用環境,執行緒數超出核心數時也會引起執行緒切換,并且作業系統對我們執行緒切換是隨機的,
2、執行緒之間如何通信
引入
- 對于我們Java語言來說,多執行緒編程也是它的特性之一,我們需要利用多執行緒操作同一
共享資源,從而實作一些特殊任務,上面說了,多執行緒在進行切換時CPU隨機調度的,假如我們直接運行多個執行緒操作共享資源的話,勢必會引起一些不可控錯誤因素, - 接下來,我們就需要讓這些不可控變為可控 !這個時候就引出了本文的重點執行緒通信,執行緒通信就是
為了解決多執行緒對同一共享變數的爭奪,
Java 執行緒通信的方式
- 共享記憶體機制
- 比如說Java的volatile關鍵字就是基于記憶體屏障解決變數的可見性,從而實作其他執行緒訪問共享變數都是必須從主存中獲取(對應其他執行緒對變數的更新也得及時的重繪到主存),
- synchronized 關鍵字基于物件鎖這種方式實作執行緒互斥,可以通知對方有其他的執行緒正在執行這部分代碼,
- 訊息傳遞模式
- wait() 和 notify()/notifyAll() 等待通知方式實作執行緒的阻塞就緒狀態之間的轉換,
- park、unpark
- join() 阻塞【底層也是依賴wait實作】,
- interrupt()打斷阻塞狀態,
- 管道輸入/輸出,
3、執行緒通信方法詳細介紹
主要介紹wait/notify,也有ReentrantLock的Condition條件變數的await/signal,LockSupport的park/unpark方法,也能實作執行緒之間的通信,主要是阻塞/喚醒通信模式,
首先說明這種方法一般都是作用于呼叫方法的所在執行緒,比如在主執行緒執行wait方法,就是將主執行緒阻塞了,
wait/notify機制
- wait()、notify方法在Java中是Object提供給我們的,又因為所有的類都默認隱式繼承了Object類,進而我們的每一個物件都具有wait和notify,
- wait方法含義:一個執行緒一旦呼叫了任意物件obj.wait()方法,它就釋放了所持有的監視器物件(obj)上的鎖,并轉為非運行狀態(阻塞),
- notify方法含義:一個執行緒若執行obj.notify方法,則隨機喚醒obj物件上監視器(作業系統也稱為管程)monitor的阻塞佇列waitset中一個執行緒,
- wait和notify方法的使用同時必須配合synchronized關鍵字使用,同時也需要成對出現,就是說wait和notify必須得在同步代碼塊內部使用,大致原因就是需要保證同時只有一個執行緒可以去執行wait,使該執行緒阻塞,
await/signal
- 要想使用await/signal首先是需要借用Condition條件變數,要想獲取Condition條件變數,就必須通過ReentrantLock鎖獲取,
- ReentrantLock和Synchronized類似,都是可重入鎖,并且大多都是當做重量級鎖使用,
- 區別:ReentrantLock是API層面實作的,我們可以根據自己隨意呼叫定制,但是Synchronized是JVM底層實作,我們無需關心他上鎖解鎖的流程,
- await/signal使用時需要配合ReentrantLock鎖物件的lock和unlock方法加鎖解鎖,就像wait/notify在synchronized在同步代碼塊中使用一樣,他們都需要保證當前執行緒是唯一執行這段邏輯的執行緒,防止出現多執行緒造成的執行緒安全問題,
park/unpark
二、執行緒通信程序中需要注意的問題
1、喚醒丟失
如果一個執行緒先于被通知執行緒呼叫wait()前呼叫了notify(),等待的執行緒將錯過這個信號,
- 喚醒丟失主要是在我們使用wait 和 notify的程序中的時序問題,比如說我們執行緒二在執行某個物件notify的時候,執行緒一還沒有執行該物件的wait方法,那么這次的喚醒就會丟失,我們就不能讓執行緒二得notify方法起作用,自然而然執行緒一就不會被喚醒,
- 舉個例子吧,這就好比我們平常在宿舍每天都會有叫醒服務,但是這次 因為一些原因(通宵···)我一整晚都沒有睡覺,而且當第二天早上的叫醒服務來的 時候也是醒著的,那么叫醒服務就會以為你已經醒來了,就會視而不見,沒想到吧,叫醒服務剛走我就躺下來睡著了,所以我錯過了這次叫醒服務,就能好好的睡億覺了,這看起來沒有什么大問題,但是你仔細想想若是每個睡著的人都需要被叫醒服務才能醒過來,外加上只有一次叫醒服務的機會,那么你就可以沉睡萬年了,開心不,
- 哈哈哈···
- 這在程式中也是一樣 的,如果錯過notify那么就會一直wait,
- 所以我們必須預防這種問題,比如說每隔一段時間去喚醒,也就是隔兩分鐘就去叫醒睡著的人,但是這種缺點就是太累了,對于程式來說是消耗性能和記憶體,實作也簡單就是寫入while回圈體中,不停地嘗試即可,
- 我們也可以使用一個標志位完美的實作,初始化設定
flag=FALSE表示還沒wait,在wait之前將設定flag=TRUE,在notify之后設定flag=FALSE,每次notify喚醒之前都判斷flag=true是否已經wait,在wait中判斷flag=false是否已經notify,
核心代碼演示
- 首先使用執行緒池創建執行緒一使自己進入阻塞態,然后再呼叫LOCK1的notify方法喚醒執行緒一
// 執行緒一使用LOCK1物件呼叫wait方法阻塞自己
executor.execute(new ThreadTest("執行緒一",LOCK1,LOCK2));
synchronized (LOCK1) {
System.out.println("main執行notify方法讓執行緒一醒過來");
LOCK1.notify();
}
-
但是他很有可能醒不來,因為主執行緒呼叫LOCK1物件的notify方法,可能主執行緒已經執行完了,上面執行緒還沒創建完成,也就是沒有進入wait狀態,就醒不來了,
-
解決方式:使用信號量標志進行判斷是否已經進入wait
synchronized (LOCK1) { while (true) { if (FLAG.getFlag()) { System.out.println("main馬上執行notify方法讓執行緒一醒過來" + "flag = " + FLAG.getFlag()); LOCK1.notify(); // 將標志位變為FALSE FLAG.setFlag(Constants.WaitOrNoWait.NO_WAIT.getFlag()); System.out.println("main執行notify方法完畢" + "flag = " + FLAG.getFlag()); break; } } }
2、假喚醒
由于莫名其妙的原因,執行緒有可能在沒有呼叫過notify()和notifyAll()的情況下醒來,
- 其實在上面的代碼中已經解決了假喚醒的問題,因為我們只需要不斷去嘗試獲取標志位資訊即可,
3、多執行緒喚醒
- 多個執行緒執行時,防止notifyAll全部喚醒之后就結束運行,我們的需求是只能喚醒一個執行緒,當其他執行緒被喚醒之后需要重新判斷標志位是否為FALSE,也就是需要判斷是否有其他執行緒執行了喚醒操作,因為一次只能叫醒一個人,需要排隊,他們就可以繼續自旋判斷,
synchronized (waitName) {
while (!flag.getFlag()) {
try {
// 將標志位設定為TRUE
flag.setFlag(Constants.WaitOrNoWait.WAIT.getFlag());
System.out.println("name;"+name+" 我睡著了進入阻塞狀態" + "flag = " + flag.getFlag());
waitName.wait();
System.out.println("name;"+name+" 我醒來了" + "flag = " + flag.getFlag());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 大家如果使用的是new Thread()方式創建執行緒的話,要想保證安全的話還可以給該標志位加上volatile關鍵字,可以時刻保證該標志位的可見性,
- 我這里使用的標志位是使用傳遞參考的方式,使用同一個物件,將標志位定義為該物件中的屬性,然后再結合列舉類進行設定標志位的值,因為我使用執行緒池創建物件,并且自定義執行緒類,這里是無法設定全域變數,傳遞給執行緒類,包裝類也不行哦,(感興趣可以親自試一下)
- 大體代碼結構如下所示:
private final static Object LOCK1 = new Object();
private final static Object LOCK2 = new Object();
private final static Constants.WaitStatus FLAG = new Constants.WaitStatus(false);
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 1, TimeUnit.DAYS, new ArrayBlockingQueue<>(4), new ThreadPoolExecutor.AbortPolicy());
executor.execute(new ThreadTest("執行緒一",LOCK1,LOCK2, FLAG));
// ···喚醒
}
class ThreadTest implements Runnable { //阻塞··· }
完整代碼可以看這[Gitee倉庫完整代碼][https://gitee.com/malongfeistudy/javabase/tree/master/Java多執行緒_Study/src/main/java/com/mlf/thread/demo_wait_notify]
三、執行緒通信實戰
前置知識:執行緒池的使用方法
-
首先復習一下創建執行緒的幾種方式和其的優缺點:
- 通過new Thread()
- 繼承Thread():和new Thread沒啥區別,就是耦合度低了
- 定義執行緒類繼承Thread類并且重寫run方法即可,
- 優點是簡潔方便
- 缺點是占用了該類的單繼承位置,無法繼承其他父類
- 實作Runnable介面
- 實作Callable介面
- 和實作Runnable介面類似
- 優點:
- 實作介面,不占用繼承的位置;
- 耦合度降低,并且可定化程度提高,各個模塊之間的呼叫關系更加清晰
- 缺點:
- 實作起來稍微麻煩
-
使用執行緒池的步驟
- 執行緒池初始化方式:
- 使用Executor初始化執行緒池
- 優點:方便快捷,適用于自己測驗時使用
- 缺點:在實際開發中無法判斷細節
- new ThreadPoolExecutor()構造器創建(本文使用方式)
- 優點:可以清晰地定制出適合自己的執行緒池,不會造成資源浪費
- 缺點:麻煩
- 使用Executor初始化執行緒池
- 執行緒池初始化方式:
-
在主執行緒自定義執行緒池使用實體,這里需要根據實際情況定義鎖物件,因為我們需要使用這些鎖物件控制多執行緒之間的運行順序以及執行緒之間的通信,在Java中每個物件都會在初始化的時候擁有一個監視器,我們需要利用好他進行并發編程,這種創建執行緒池的方法也是阿里巴巴推薦的方式,想想以阿里的體量多年總結出來的總沒有錯,大家還是提前約束自己的編碼習慣等,安裝一個阿里代碼規范的插件對自己的程式員道路是比較nice的,
/**
* 每個使用對應唯一的物件作為監視器物件鎖,
*/
public static final Object A_O = new Object();
public static final Object B_O = new Object();
/** 引數:
* int corePoolSize, 核心執行緒數
* int maximumPoolSize, 最大執行緒數
* long keepAliveTime, 救急存活時間
* TimeUnit unit, 單時間位
* BlockingQueue<Runnable> workQueue, 阻塞佇列
* RejectedExecutionHandler handler 拒絕策略
**/
// 使用阿里巴巴推薦的創建執行緒池的方式
// 通過ThreadPoolExecutor建構式自定義引數創建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
3,
5,
1,
TimeUnit.DAYS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.AbortPolicy());
- 接下來需要自定義執行緒類,我們可以自定義執行緒類,并且在該執行緒類中定義自己需要的共享資源(鎖物件,屬性等),在run方法中寫盡自己的執行緒運行邏輯即可,
class ThreadDiy implements Runnable {
private final String name;
/**
* 阻塞鎖物件 等待標記
**/
private final Object waitFor;
/**
* 執行鎖物件 下一個標記
**/
private final Object next;
public AlternateThread(String name, Object waitFor, Object next) {
}
@Override
public void run() {
// 執行緒的代碼邏輯···
}
}
1、控制兩個執行緒之間的執行順序
題目:現在有兩個執行緒,不論執行緒的啟動順序,我需要指定執行緒一先執行,然后執行緒二再執行,
-
初始化兩個物件鎖作為執行緒監視器,
private final static Object ONE_LOCK = new Object(); private final static Object TWO_LOCK = new Object(); -
接下來初始化執行緒池,上面有具體的介紹,在這就不多說了
-
使用執行緒池去執行我們的兩個執行緒,在這里我們需要分析的是
// 使用執行緒池創建執行緒
executor.execute(new DiyThread(1, ONE_LOCK, TWO_LOCK));
executor.execute(new DiyThread(2, TWO_LOCK, ONE_LOCK));
synchronized (ONE_LOCK) {
ONE_LOCK.notify();
}
創建執行緒類
-
我們使用繼承Runnable的方式去創建執行緒物件,需要在這個類中實作每個執行緒執行的邏輯,我們根據題目可以得出,我們要控制每個執行緒的執行順序,怎么辦?那么就要實作所有執行緒之間的通信,通信方式采用wait-notify的方式即可,我們使用wait-notify的時候必須結合synchronized,那么就需要控制兩個物件鎖,因為我們不光是控制自己,還有另一個執行緒,
-
我們再分析一下題意,首先需要指定先后執行的順序,那么就需要實作兩個執行緒之間的通信,其次呢,我們得控制兩個執行緒,那么就需要兩個監視器去監視這兩個執行緒,
-
我們定義這兩個監視器物件為own和other,然后再新增一個屬性threadId來標識自己,
private final int threadId; private final Object own; private final Object other; -
接下來就是撰寫Run方法了
-
每個執行緒首先需要阻塞自己,等待喚醒,然后喚醒之后,再去喚醒另外一個執行緒,這樣就實作了自定義順序,至于先喚醒哪個執行緒,交給我們的主執行緒去完成,
-
這里需要注意的是,如果我們只是單純地執行了多個執行緒物件,但是主執行緒沒有主動去喚醒其中一個,這樣就會形成類似于死鎖的回圈等待,你需要我喚醒,我需要你喚醒,這個時候需要主執行緒去插手喚醒其中的任意一個執行緒,
-
第一步阻塞自己own
synchronized (own) { try { own.wait(); System.out.println(num); } catch (InterruptedException e) { e.printStackTrace(); } } -
第二步喚醒other
synchronized (other) { other.notify(); }
-
2、多執行緒交替列印輸出
題目需求:現在需要使用三個執行緒輪流列印輸出,說白了也就是多執行緒輪流執行罷了,和問題一控制兩個執行緒列印順序沒什么區別
- 還是老步驟,首先需要定義執行緒類,我們需要控制當前執行緒和下一個執行緒即可,我們這里需要兩個物件,一個是阻塞鎖物件用來阻塞當前執行緒,另一個是喚醒鎖物件,用來喚醒下一個物件,
/**
* 阻塞鎖物件 等待標記
**/
private final Object waitFor;
/**
* 喚醒鎖物件 下一個標記
**/
private final Object next;
-
run方法的邏輯和上面的基本一樣, 一個執行緒一旦呼叫了任意物件的wait()方法,它就釋放了所持有的監視器物件上的鎖,并轉為非運行狀態,
-
每個執行緒首先會呼叫 waitFor物件的 wait()方法,隨后該執行緒進入阻塞狀態,等待其他執行緒執行自己參考的該 waitFor物件的 notify()方法即可,
while (true) { synchronized (waitFor) { try { waitFor.wait(); System.out.println(name + " 開始執行"); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (next) { next.notify(); } } -
主執行緒需要初始化執行緒池、執行三個執行緒,并且最后需要打破僵局,因為此時每個執行緒都是阻塞狀態,他們沒法阻塞/喚醒回圈下去,
synchronized (A_O) { A_O.notify(); } -
模擬執行流程
/**
* 模擬執行流程
* 列印名(name) 等待標記(waitFor) 下一個標記(next)
* 1 A B
* 2 B C
* 3 C A
*
* 像不像Spring的回圈依賴:確實很像,Spring中的回圈依賴就是 BeanA 依賴 BeanB,BeanB 依賴 BeanA;
* 他們實體化程序中都需要先屬性注入對方的實體,倘若剛開始的時候都沒有實體化,初始化就會死等,類似于死鎖,
**/
3、多執行緒順序列印同一個自增變數
使用多執行緒輪流列印 01234····
- 思路:使用自增原子變數AtomicInteger和多執行緒配合列印,
具體代碼請移步到Gitee倉庫:[順序列印自增變數][https://gitee.com/malongfeistudy/javabase/blob/master/Java多執行緒_Study/src/main/java/com/mlf/thread/print/AddNumberPrint2.java]
條件變數Condition的使用
- Condition是一個 LOCK 實體出來的,他們獲取的都是一個 LOCK 的鎖,而如果要呼叫 object的 wait和notify 方法,首先要獲取對應的object的鎖,如果要呼叫Condition 的await、signal方法,必須先獲取Lock鎖(Lock.lock),
- 多執行緒的初衷就是操作共享資源,然后我們需要保證共享資源同一時刻只能被一個執行緒所修改,那么就需要一把鎖來控制這些執行緒之間互斥條件,這里使用一個ReentrantLock鎖作為我們的Lock物件,通過同一個 Lock鎖 獲取的每個Condition 就可以作為每個執行緒自己的阻塞條件和喚醒條件,
如有問題,請留言評論,
作者:小白且菜鳥出處:https://www.cnblogs.com/malongfeistudy/轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/527990.html
標籤:其他
