深入理解Java中線程間的通信
合理的使用Java多線程可以更好地利用服務(wù)器資源。一般來講,線程內(nèi)部有自己私有的線程上下文,互不干擾。但是當(dāng)我們需要多個線程之間相互協(xié)作的時候,就需要我們掌握J(rèn)ava線程的通信方式。本文將介紹Java線程之間的幾種通信原理。
鎖與同步
在Java中,鎖的概念都是基于對象的,所以我們又經(jīng)常稱它為對象鎖。一個鎖同一時間只能被一個線程持有。也就是說,一個鎖如果被一個線程所持有,那其他線程如果需要得到這個鎖,就得等這個線程釋放該鎖。
線程之間,有一個同步的概念。在多線程中,可能有多個線程試圖訪問一個有限的資源,必須預(yù)防這種情況的發(fā)生。
所以引入了同步機制:在線程使用一個資源時為其加鎖,這樣其他的線程便不能訪問那個資源了,直到解鎖后才可以訪問。線程同步是線程之間按照一定的順序執(zhí)行。
我們先來看看一個無鎖的程序:
public class NoLock { static class ThreadA implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("Thread A " + i); } } } static class ThreadB implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("Thread B " + i); } } } public static void main(String[] args) { new Thread(new ThreadA()).start(); new Thread(new ThreadB()).start(); } }
執(zhí)行這個程序,你會在控制臺看到,線程A和線程B各自獨立工作,輸出自己的打印值。每一次運行結(jié)果都會不一樣。如下是我的電腦上某一次運行的結(jié)果。
...
Thread B 74
Thread B 75
Thread B 76
Thread B 77
Thread B 78
Thread B 79
Thread B 80
Thread A 3
Thread A 4
Thread A 5
Thread A 6
Thread A 7
Thread A 8
Thread A 9
...
現(xiàn)在有一個需求,想等A先執(zhí)行完之后,再由B去執(zhí)行,怎么辦呢?最簡單的方式就是使用一個“對象鎖”。
public class ObjLock { private static Object lock = new Object(); static class ThreadA implements Runnable { @Override public void run() { synchronized (lock) { for (int i = 0; i < 100; i++) { System.out.println("Thread A " + i); } } } } static class ThreadB implements Runnable { @Override public void run() { synchronized (lock) { for (int i = 0; i < 100; i++) { System.out.println("Thread B " + i); } } } } public static void main(String[] args) throws InterruptedException { new Thread(new ThreadA()).start(); Thread.sleep(10); new Thread(new ThreadB()).start(); } }
這里聲明了一個名字為lock
的對象鎖。我們在ThreadA
和ThreadB
內(nèi)需要同步的代碼塊里,都是用synchronized
關(guān)鍵字加上了同一個對象鎖lock
。
上文我們說到了,根據(jù)線程和鎖的關(guān)系,同一時間只有一個線程持有一個鎖,那么線程B就會等線程A執(zhí)行完成后釋放lock
,線程B才能獲得鎖lock
。
這里在主線程里使用sleep方法睡眠了10毫秒,是為了防止線程B先得到鎖。因為如果同時start,線程A和線程B都是出于就緒狀態(tài),操作系統(tǒng)可能會先讓B運行。這樣就會先輸出B的內(nèi)容,然后B執(zhí)行完成之后自動釋放鎖,線程A再執(zhí)行。
等待/通知機制
上面一種基于“鎖”的方式,線程需要不斷地去嘗試獲得鎖,如果失敗了,再繼續(xù)嘗試。這可能會耗費服務(wù)器資源。 而等待/通知機制是另一種方式。
Java多線程的等待/通知機制是基于Object
類的wait()
方法和notify()
, notifyAll()
方法來實現(xiàn)的。
notify()方法會隨機叫醒一個正在等待的線程,而notifyAll()會叫醒所有正在等待的線程。
前面我們講到,一個鎖同一時刻只能被一個線程持有。而假如線程A現(xiàn)在持有了一個鎖lock
并開始執(zhí)行,它可以使用lock.wait()
讓自己進入等待狀態(tài)。這個時候,lock
這個鎖是被釋放了的。
這時,線程B獲得了lock
這個鎖并開始執(zhí)行,它可以在某一時刻,使用lock.notify()
,通知之前持有lock
鎖并進入等待狀態(tài)的線程A,說“線程A你不用等了,可以往下執(zhí)行了”。
需要注意的是,這個時候線程B并沒有釋放鎖lock
,除非線程B這個時候使用lock.wait()
釋放鎖,或者線程B執(zhí)行結(jié)束自行釋放鎖,線程A才能得到lock
鎖。
用代碼來實現(xiàn)一下:
public class WaitNotify { private static Object lock = new Object(); static class ThreadA implements Runnable { @Override public void run() { synchronized (lock) { for (int i = 0; i < 5; i++) { try { System.out.println("ThreadA: " + i); lock.notify(); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } lock.notify(); } } } static class ThreadB implements Runnable { @Override public void run() { synchronized (lock) { for (int i = 0; i < 5; i++) { try { System.out.println("ThreadB: " + i); lock.notify(); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } lock.notify(); } } } public static void main(String[] args) throws InterruptedException { new Thread(new ThreadA()).start(); Thread.sleep(1000); new Thread(new ThreadB()).start(); } } // 輸出: ThreadA: 0 ThreadB: 0 ThreadA: 1 ThreadB: 1 ThreadA: 2 ThreadB: 2 ThreadA: 3 ThreadB: 3 ThreadA: 4 ThreadB: 4
線程A和線程B首先打印出自己需要的東西,然后使用notify()
方法叫醒另一個正在等待的線程,然后自己使用wait()
方法陷入等待并釋放lock
鎖。
需要注意的是等待/通知機制使用的是使用同一個對象鎖,如果你兩個線程使用的是不同的對象鎖,那它們之間是不能用等待/通知機制通信的。
信號量--Volatile
信號量(Semaphore) :有時被稱為信號燈,是在多線程環(huán)境下使用的一種設(shè)施,是可以用來保證兩個或多個關(guān)鍵代碼段不被并發(fā)調(diào)用。在進入一個關(guān)鍵代碼段之前,線程必須獲取一個信號量;一旦該關(guān)鍵代碼段完成了,那么該線程必須釋放信號量。其它想進入該關(guān)鍵代碼段的線程必須等待直到第一個線程釋放信號量。
本文不是要介紹這個類,而是介紹一種基于volatile
關(guān)鍵字的自己實現(xiàn)的信號量通信。
volatile關(guān)鍵字能夠保證內(nèi)存的可見性,如果用volatile關(guān)鍵字聲明了一個變量,在一個線程里面改變了這個變量的值,那其它線程是立馬可見更改后的值的。
比如我現(xiàn)在有一個需求,我想讓線程A輸出0,然后線程B輸出1,再然后線程A輸出2…以此類推。我應(yīng)該怎樣實現(xiàn)呢?
public class Count { private static volatile int count = 0; static class ThreadA implements Runnable { @Override public void run() { while (count < 5) { if (count % 2 == 0) { System.out.println("threadA: " + count); synchronized (this) { count++; } } } } } static class ThreadB implements Runnable { @Override public void run() { while (count < 5) { if (count % 2 == 1) { System.out.println("threadB: " + signal); synchronized (this) { count++; } } } } } public static void main(String[] args) throws InterruptedException { new Thread(new ThreadA()).start(); Thread.sleep(1000); new Thread(new ThreadB()).start(); } } // 輸出: threadA: 0 threadB: 1 threadA: 2 threadB: 3 threadA: 4
我們可以看到,使用了一個volatile
變量count
來實現(xiàn)了“信號量”的模型。這里需要注意的是,volatile
變量需要進行原子操作,而count++
并不是一個原子操作,根據(jù)需要使用synchronized
給它“上鎖”,或者是使用AtomicInteger
等原子類。
信號量的應(yīng)用場景
假如在一個停車場中,車位是我們的公共資源,線程就如同車輛,而看門的管理員就是起的“信號量”的作用。 因為在這種場景下,多個線程需要相互合作,我們用簡單的“鎖”和“等待通知機制”就不那么方便了。這個時候就可以用到信號量。
管道輸入/輸出流
管道是基于“管道流”的通信方式。JDK提供了PipedWriter
、 PipedReader
、 PipedOutputStream
、 PipedInputStream
。
其中,前面兩個是基于字符的,后面兩個是基于字節(jié)流的。
以下示例代碼使用的是基于字符的:
public class Pipe { static class ReaderThread implements Runnable { private PipedReader reader; public ReaderThread(PipedReader reader) { this.reader = reader; } @Override public void run() { System.out.println("this is reader"); int receive = 0; try { while ((receive = reader.read()) != -1) { System.out.print((char)receive); } } catch (IOException e) { e.printStackTrace(); } } } static class WriterThread implements Runnable { private PipedWriter writer; public WriterThread(PipedWriter writer) { this.writer = writer; } @Override public void run() { System.out.println("this is writer"); int receive = 0; try { writer.write("test"); } catch (IOException e) { e.printStackTrace(); } finally { try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws IOException, InterruptedException { PipedWriter writer = new PipedWriter(); PipedReader reader = new PipedReader(); writer.connect(reader); new Thread(new ReaderThread(reader)).start(); Thread.sleep(1000); new Thread(new WriterThread(writer)).start(); } } // 輸出: this is reader this is writer test
我們通過線程的構(gòu)造函數(shù),傳入了PipedWrite
和PipedReader
對象??梢院唵畏治鲆幌逻@個示例代碼的執(zhí)行流程:
- 線程ReaderThread開始執(zhí)行,
- 線程ReaderThread使用管道reader.read()進入”阻塞“,
- 線程WriterThread開始執(zhí)行,
- 線程WriterThread用writer.write("test")往管道寫入字符串,
- 線程WriterThread使用writer.close()結(jié)束管道寫入,并執(zhí)行完畢,
- 線程ReaderThread接受到管道輸出的字符串并打印,
- 線程ReaderThread執(zhí)行完畢。
管道通信的應(yīng)用場景
這個很好理解。使用管道多半與I/O流相關(guān)。當(dāng)我們一個線程需要先另一個線程發(fā)送一個信息(比如字符串)或者文件等等時,就需要使用管道通信了。
Thread.join()方法
join()
方法是Thread類的一個實例方法。它的作用是讓當(dāng)前線程陷入“等待”狀態(tài),等join的這個線程執(zhí)行完成后,再繼續(xù)執(zhí)行當(dāng)前線程。
有時候,主線程創(chuàng)建并啟動了子線程,如果子線程中需要進行大量的耗時運算,主線程往往將早于子線程結(jié)束之前結(jié)束。
如果主線程想等待子線程執(zhí)行完畢后,獲得子線程中的處理完的某個數(shù)據(jù),就要用到j(luò)oin方法了。
示例代碼:
public class Join { static class ThreadA implements Runnable { @Override public void run() { try { System.out.println("我是子線程,我先睡一秒"); Thread.sleep(1000); System.out.println("我是子線程,我睡完了一秒"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new ThreadA()); thread.start(); thread.join(); System.out.println("如果不加join方法,我會先被打出來,加了就不一樣了"); } }
ThreadLocal類
ThreadLocal是一個本地線程副本變量工具類。內(nèi)部是一個弱引用的Map來維護。
嚴(yán)格來說,ThreadLocal類并不屬于多線程間的通信,而是讓每個線程有自己”獨立“的變量,線程之間互不影響。它為每個線程都創(chuàng)建一個副本,每個線程可以訪問自己內(nèi)部的副本變量。
ThreadLocal類最常用的就是set方法和get方法。示例代碼:
public class Profiler { private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() { protected Long initialValue() { return System.currentTimeMillis(); } }; public static void begin() { TIME_THREADLOCAL.set(System.currentTimeMillis()); } public static long end() { return System.currentTimeMillis() - TIME_THREADLOCAL.get(); } public static void main(String[] args) throws InterruptedException { Profiler.begin(); TimeUnit.SECONDS.sleep(1); System.out.println("耗時:" + Profiler.end() + "mills"); } } // 輸出: 耗時:1001mills
Profiler
可以被復(fù)用在方法的耗時統(tǒng)計的功能上,在方法的入口前執(zhí)行begin()
方法,在方法調(diào)用后執(zhí)行end()
方法,好處是兩個方法的調(diào)用不用在一個方法和類中,比如在AOP(面向切面編程)中,可以在方法調(diào)用前的切入點執(zhí)行begin()
方法,而在方法調(diào)用切入點執(zhí)行end()
方法,這樣依舊可以獲得方法的執(zhí)行耗時。
ThreadLocal的應(yīng)用場景
最常見的ThreadLocal使用場景為用來解決數(shù)據(jù)庫連接、Session管理等。數(shù)據(jù)庫連接和Session管理涉及多個復(fù)雜對象的初始化和關(guān)閉。如果在每個線程中聲明一些私有變量來進行操作,那這個線程就變得不那么“輕量”了,需要頻繁的創(chuàng)建和關(guān)閉連接。
小結(jié)
線程間通信使線程成為一個整體,提高系統(tǒng)之間的交互性,在提高CPU利用率的同時可以對線程任務(wù)進行有效的把控與監(jiān)督。
以上就是深入理解Java中線程間的通信的詳細(xì)內(nèi)容,更多關(guān)于Java線程通信的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
spring security結(jié)合jwt實現(xiàn)用戶重復(fù)登錄處理
本文主要介紹了spring security結(jié)合jwt實現(xiàn)用戶重復(fù)登錄處理,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03詳細(xì)總結(jié)Java創(chuàng)建文件夾的方法及優(yōu)缺點
很多小伙伴都不知道如何用Java創(chuàng)建文件夾,今天給大家整理了這篇文章,文中有非常詳細(xì)的方法介紹及方法的優(yōu)缺點,對正在學(xué)習(xí)java的小伙伴們有很好地幫助,需要的朋友可以參考下2021-05-05加速spring/springboot應(yīng)用啟動速度詳解
這篇文章主要介紹了加速spring/springboot應(yīng)用啟動速度詳解,本文通過實例代碼給大家介紹的非常詳細(xì)對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07Java線程池并發(fā)執(zhí)行多個任務(wù)方式
這篇文章主要介紹了Java線程池并發(fā)執(zhí)行多個任務(wù)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08Java客戶端通過HTTPS連接到Easysearch實現(xiàn)過程
這篇文章主要為大家介紹了Java客戶端通過HTTPS連接到Easysearch實現(xiàn)過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11