在多線程系統中,彼此之間的通信協作非常重要,下面來聊聊線程間通信的幾種方式。
wait/notify
想像一個場景,A、B兩個線程操作一個共享List對象,A對List進行add操作,B線程等待List的size=500時就打印記錄日志,這要怎么處理呢?
一個辦法就是,B線程while (true) { if(List.size == 500) {打印日志} },這樣兩個線程之間就有了通信,B線程不斷通過輪訓來檢測 List.size == 500 這個條件。
這樣可以實現我們的需求,但是也帶來了問題:CPU把資源浪費了B線程的輪詢操作上,因為while操作並不釋放CPU資源,導致了CPU會一直在這個線程中做判斷操作。
這要非常浪費CPU資源,所以就需要有一種機制來實現減少CPU的資源浪費,而且還可以實現在多個線程間通信,它就是“wait/notify”機制。
定義兩個線程類:
public class MyThread1_1 extends Thread {
private Object lock;
public MyThread1_1(Object lock) {
this.lock = lock;
}
public void run() {
try {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "開始------wait time = " + System.currentTimeMillis());
lock.wait();
System.out.println(Thread.currentThread().getName() + "開始------sleep time = " + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "結束------sleep time = " + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() + "結束------wait time = " + System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MyThread1_2 extends Thread {
private Object lock;
public MyThread1_2(Object lock) {
this.lock = lock;
}
public void run() {
try {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "開始------notify time = " + System.currentTimeMillis());
lock.notify();
System.out.println(Thread.currentThread().getName() + "開始------sleep time = " + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "結束------sleep time = " + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() + "結束------notify time = " + System.currentTimeMillis());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
測試方法,myThread1先執行,然后sleep 一秒后,myThread2再執行
@Test
public void test1() throws InterruptedException {
Object object = new Object();
MyThread1_1 myThread1_1 = new MyThread1_1(object);
MyThread1_2 myThread1_2 = new MyThread1_2(object);
myThread1_1.start();
Thread.sleep(1000);
myThread1_2.start();
myThread1_1.join();
myThread1_2.join();
}
執行結果:
Thread-0開始------wait time = 1639464183921 Thread-1開始------notify time = 1639464184925 Thread-1開始------sleep time = 1639464184925 Thread-1結束------sleep time = 1639464186928 Thread-1結束------notify time = 1639464186928 Thread-0開始------sleep time = 1639464186928 Thread-0結束------sleep time = 1639464188931 Thread-0結束------wait time = 1639464188931
可以看到第一行和第二行 開始執行之間只間隔了1s,說明wait方法確實進入等待,
而且沒有繼續執行wait后面的sleep 2秒,而是執行了notify方法,說明wait方法可以使調用該方法的線程釋放共享資源的鎖,然后從運行狀態退出,進入等待隊列,直到被再次喚醒。
第二行和第五行間隔2秒鍾,說明notify方法不會釋放共享資源的鎖。
第6行 說明notify執行完后,喚醒了剛才wait的線程,從而繼續執行后面的sleep方法。
說明notify方法可以隨機喚醒等待隊列中等待同一共享資源的“一個”線程,並使該線程退出等待隊列,進入可運行狀態,也就是notify()方法僅通知“一個”線程。
另外還有notifyAll()方法可以使所有正在等待隊列中等待同一共享資源的“全部”線程從等待狀態退出,進入可運行狀態。
此時,優先級最高的那個線程最先執行,但也有可能是隨機執行,因為這要取決於JVM虛擬機的實現。
方法join
前面的測試方法中幾乎都使用了join方法,那么這個方法到底起到什么作用呢?
在很多情況下,主線程創建並啟動子線程,如果子線程中要進行大量的耗時運算,主線程往往將早於子線程結束之前結束,
所以在主線程中使用join方法的作用就是讓主線程等待子線程線程對象銷毀。
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
看下jdk API的源碼可以看到,其實join內部使用的還是wait方法進行等待,
join(long millis)方法的一個重點是要區分出和sleep(long millis)方法的區別:
sleep(long millis)不釋放鎖,join(long millis)釋放鎖,因為join方法內部使用的是wait(),因此會釋放鎖。join()其實就是join(0)而已。
ThreadLocal類
ThreadLocal不是用來解決共享對象的多線程訪問問題的,而是實現每一個線程都維護自己的共享變量,起到線程隔離的作用。
關於ThreadLocal源碼分析可以參考這篇文章:https://www.cnblogs.com/xrq730/p/4854813.html。
下面看個ThreadLocal的例子:
public class Tools {
public static ThreadLocal<Object> tl = new ThreadLocal<Object>();
}
兩個線程類,分別向ThreadLocal里設置值
public class MyThread1_1 extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
Tools.tl.set("ThreadA" + (i + 1));
System.out.println("ThreadA get Value=" + Tools.tl.get());
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class MyThread1_2 extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
Tools.tl.set("ThreadB" + (i + 1));
System.out.println("ThreadB get Value=" + Tools.tl.get());
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Test
public void test1() {
try {
MyThread1_1 a = new MyThread1_1();
MyThread1_2 b = new MyThread1_2();
a.start();
b.start();
a.join();
b.join();
} catch (Exception e) {
e.printStackTrace();
}
}
執行結果:
ThreadB get Value=ThreadB1 ThreadA get Value=ThreadA1 ThreadA get Value=ThreadA2 ThreadB get Value=ThreadB2 ThreadA get Value=ThreadA3 ThreadB get Value=ThreadB3 ThreadA get Value=ThreadA4 ThreadB get Value=ThreadB4 ThreadB get Value=ThreadB5 ThreadA get Value=ThreadA5 ThreadB get Value=ThreadB6 ThreadA get Value=ThreadA6 ThreadB get Value=ThreadB7 ThreadA get Value=ThreadA7 ThreadB get Value=ThreadB8 ThreadA get Value=ThreadA8 ThreadA get Value=ThreadA9 ThreadB get Value=ThreadB9 ThreadB get Value=ThreadB10 ThreadA get Value=ThreadA10
可以看到兩個線程取出的值沒有重復也沒有互相影響,其實它內部變化的只是線程本身的 ThreadLocalMap。
感興趣的還可以去看看 InheritableThreadLocal,它可以在子線程中取得父線程繼承下來的值。
參考文獻
1:《Java並發編程的藝術》
2:《Java多線程編程核心技術》
