線程間的通信
1、爲什麼需要線程通信
線程是操作系統調度的最小單位,有自己的棧空間,可以按照既定的代碼逐步的執行,但是如果每個線程間都孤立的運行,那就會造資源浪費。所以在現實中,我們需要這些線程間可以按照指定的規則共同完成一件任務,所以這些線程之間就需要互相協調,這個過程被稱爲線程的通信。
線程的通信可以被定義爲:
線程通信就是當多個線程共同操作共享的資源時,互相告知自己的狀態以避免資源爭奪。
2、線程通信的方式
線程通信主要可以分爲三種方式,分別爲共享內存、消息傳遞和管道流。每種方式有不同的方法來實現
- 共享內存:線程之間共享程序的公共狀態,線程之間通過讀-寫內存中的公共狀態來隱式通信。
volatile共享內存
- 消息傳遞:線程之間沒有公共的狀態,線程之間必須通過明確的發送信息來顯示的進行通信。
wait/notify等待通知方式
join方式
- 管道流
管道輸入/輸出流的形式
2.1共享內存
在學習Volatile之前,我們先了解下Java的內存模型,
在java中,所有堆內存中的所有的數據(實例域、靜態域和數組元素)存放在主內存中可以在線程之間共享,一些局部變量、方法中定義的參數存放在本地內存中不會在線程間共享。線程之間的共享變量存儲在主內存中,本地內存存儲了共享變量的副本。如果線程A要和線程B通信,則需要經過以下步驟
①線程A把本地內存A更新過的共享變量刷新到主內存中
②線程B到內存中去讀取線程A之前已更新過的共享變量。
這保證了線程間的通信必須經過主內存。下面引出我們要學習的關鍵字volatile
volatile有一個關鍵的特性:保證內存可見性,即多個線程訪問內存中的同一個被volatile關鍵字修飾的變量時,當某一個線程修改完該變量後,需要先將這個最新修改的值寫回到主內存,從而保證下一個讀取該變量的線程取得的就是主內存中該數據的最新值,這樣就保證線程之間的透明性,便於線程通信。
代碼實現
/**
* @Author: Simon Lang
* @Date: 2020/5/5 15:13
*/
public class TestVolatile {
private static volatile boolean flag=true;
public static void main(String[] args){
new Thread(new Runnable() {
public void run() {
while (true){
if(flag){
System.out.println("線程A");
flag=false;
}
}
}
}).start();
new Thread(new Runnable() {
public void run() {
while (true){
if(!flag){
System.out.println("線程B");
flag=true;
}
}
}
}).start();
}
}
測試結果:線程A和線程B交替執行
2.2消息傳遞
2.2.1wait/notify等待通知方式
從字面上理解,等待通知機制就是將處於等待狀態的線程將由其它線程發出通知後重新獲取CPU資源,繼續執行之前沒有執行完的任務。最典型的例子生產者–消費者模式
有一個產品隊列,生產者想要在隊列中添加產品,消費者需要從隊列中取出產品,如果隊列爲空,消費者應該等待生產者添加產品後才進行消費,隊列爲滿時,生產者需要等待消費者消費一部分產品後才能繼續生產。隊列可以認爲是java模型裏的臨界資源,生產者和消費者認爲是不同的線程,它們需要交替的佔用臨界資源來進行各自方法的執行,所以就需要線程間通信。
生產者–消費者模型主要爲了方便複用和解耦,java語言實現線程之間的通信協作的方式是等待/通知機制
等待/通知機制提供了三個方法用於線程間的通信
wait() | 當前線程釋放鎖並進入等待(阻塞)狀態 |
---|---|
notify() | 喚醒一個正在等待相應對象鎖的線程,使其進入就緒隊列,以便在當前線程釋放鎖後繼續競爭鎖 |
notifyAll() | 喚醒所有正在等待相應對象鎖的線程,使其進入就緒隊列,以便在當前線程釋放鎖後繼續競爭鎖 |
等待/通知機制是指一個線程A調用了對象Object的wait()方法進入等待狀態,而另一線程B調用了對象Object的notify()或者notifyAll()方法,當線程A收到通知後就可以從對象Object的wait()方法返回,進而執行後序的操作。線程間的通信需要對象Object來完成,對象中的wait()、notify()、notifyAll()方法就如同開關信號,用來完成等待方和通知方的交互。
測試代碼
public class WaitNotify {
static boolean flag=true;
static Object lock=new Object();
public static void main(String[] args) throws InterruptedException {
Thread waitThread=new Thread(new WaitThread(),"WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread=new Thread(new NotifyThread(),"NotifyThread");
notifyThread.start();
}
//等待線程
static class WaitThread implements Runnable{
public void run() {
//加鎖
synchronized (lock){
//條件不滿足時,繼續等待,同時釋放lock鎖
while (flag){
System.out.println("flag爲true,不滿足條件,繼續等待");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//條件滿足
System.out.println("flag爲false,我要從wait狀態返回繼續執行了");
}
}
}
//通知線程
static class NotifyThread implements Runnable{
public void run() {
//加鎖
synchronized (lock){
//獲取lock鎖,然後進行通知,但不會立即釋放lock鎖,需要該線程執行完畢
lock.notifyAll();
System.out.println("設置flag爲false,我發出通知了,但是我不會立馬釋放鎖");
flag=false;
}
}
}
}
測試結果
NOTE:使用wait()、notify()和notifyAll()需要注意以下細節
- 使用wait()、notify()和notifyAll()需要先調用對象加鎖
- 調用wait()方法後,線程狀態由Running變成Waiting,並將當前線程放置到對象的等待隊列
- notify()和notifyAll()方法調用後,等待線程依舊不會從wait()返回,需要調用notify()和notifyAll()的線程釋放鎖之後等待線程纔有機會從wait()返回
- notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是將等待隊列中所有的線程全部轉移到同步隊列,被移到的線程狀態由Waiting變爲Blocked。
- 從wait()方法返回的前提是獲得調用對象的鎖
其實等待通知機制有有一個經典的範式,該範式可以分爲兩部分,分別是等待方(消費者)和通知方(生產者)
- 等待方
synchronized(對象){
while(條件不滿足){
對象.wait()
}
對應的處理邏輯
}
- 通知方
synchronized(對象){
改變條件
對象.notifyAll
}
2.2.2join方式
在很多應用場景中存在這樣一種情況,主線程創建並啓動子線程後,如果子線程要進行很耗時的計算,那麼主線程將比子線程先結束,但是主線程需要子線程的計算的結果來進行自己下一步的計算,這時主線程就需要等待子線程,java中提供可join()方法解決這個問題。
join()方法的作用是:在當前線程A調用線程B的join()方法後,會讓當前線程A阻塞,直到線程B的邏輯執行完成,A線程纔會解除阻塞,然後繼續執行自己的業務邏輯,這樣做可以節省計算機中資源。
測試代碼
public class TestJoin {
public static void main(String[] args){
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("線程0開始執行了");
}
});
thread.start();
for (int i=0;i<10;i++){
JoinThread jt=new JoinThread(thread,i);
jt.start();
thread=jt;
}
}
static class JoinThread extends Thread{
private Thread thread;
private int i;
public JoinThread(Thread thread,int i){
this.thread=thread;
this.i=i;
}
@Override
public void run() {
try {
thread.join();
System.out.println("線程"+(i+1)+"執行了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
測試結果
NOTE:每個線程的終止的前提是前驅線程的終止,每個線程等待前驅線程終止後,才從join方法返回,實際上,這裏涉及了等待/通知機制,即下一個線程的執行需要接受前驅線程結束的通知。
2.3管道輸入/輸出流
管道流是是一種使用比較少的線程間通信方式,管道輸入/輸出流和普通文件輸入/輸出流或者網絡輸出/輸出流不同之處在於,它主要用於線程之間的數據傳輸,傳輸的媒介爲管道。
管道輸入/輸出流主要包括4種具體的實現:PipedOutputStrean、PipedInputStrean、PipedReader和PipedWriter,前兩種面向字節,後兩種面向字符。
java的管道的輸入和輸出實際上使用的是一個循環緩衝數組來實現的,默認爲1024,輸入流從這個數組中讀取數據,輸出流從這個數組中寫入數據,當這個緩衝數組已滿的時候,輸出流所在的線程就會被阻塞,當向這個緩衝數組爲空時,輸入流所在的線程就會被阻塞。
buffer:緩衝數組,默認爲1024
out:從緩衝數組中讀數據
in:從緩衝數組中寫數據
測試代碼
public class TestPip {
public static void main(String[] args) throws IOException {
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
//使用connect方法將輸入流和輸出流連接起來
writer.connect(reader);
Thread printThread = new Thread(new Print(reader) , "PrintThread");
//啓動線程printThread
printThread.start();
int receive = 0;
try{
//讀取輸入的內容
while((receive = System.in.read()) != -1){
writer.write(receive);
}
}finally {
writer.close();
}
}
private static class Print implements Runnable {
private PipedReader reader;
public Print(PipedReader reader) {
this.reader = reader;
}
@Override
public void run() {
int receive = 0;
try{
while ((receive = reader.read()) != -1){
//字符轉換
System.out.print((char) receive);
}
}catch (IOException e) {
System.out.print(e);
}
}
}
}
測試結果
NOTE:對於Piped類型的流,必須先進性綁定,也就是調用connect()方法,如果沒有將輸入/輸出流綁定起來,對於該流的訪問將拋出異常。
關注公衆號:10分鐘編程,讓我們每天博學一點點點
公衆號回覆success,領取學習資料
參考文獻
[1]方騰飛.Java併發編程的藝術
[2]http://www.voidcn.com/article/p-hwhufdsx-bqo.html
[3]https://blog.csdn.net/canot/article/details/50879963