【多執行緒】執行緒通訊之join、ThreadLocal
一、方法join
在很多情況下,主執行緒建立並啟動子執行緒,如果子執行緒中需要進行大量的耗時運算,主執行緒往往將早於子執行緒結束之前結束。如果主執行緒想等待子執行緒執行完成之後在結束,比如子執行緒執行一個方法,主執行緒要取得這個方法的返回值,就要用到join()方法了。方法join的作用是等待執行緒物件銷燬。執行緒Thread除了提供join方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個具有超時特性的方法。這兩個超時方法表示,如果執行緒thread在給定的超時時間內沒有銷燬,那麼將從該超時方法中返回。
1、簡單demo
輸出結果如下:public class MyThread extends Thread { @Override public void run(){ try { int secondValue=(int)(Math.random()*10000); System.out.println(secondValue); Thread.sleep(secondValue); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { try { MyThread threadTest=new MyThread(); threadTest.start(); //Thread.sleep(?) threadTest.join(); System.out.println("我想當threadTest物件執行完畢後我再執行,我做到了"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
方法join的作用是使所屬的執行緒物件x正常執行run方法中的任務,而使當前執行緒z進入阻塞狀態,等待執行緒x銷燬後再繼續執行執行緒z後面的程式碼。
方法join具有使執行緒排隊執行的作用,有些類似同步的效果。join與synchronized的區別是:join在內部使用wait()方法進行等待,而synchronized關鍵字使用的是“物件監視器”原理作為同步。
2、使執行緒按順序執行
之前說過執行緒呼叫具有“無序性”的特點,那麼 有三個執行緒T1,T2、T3,怎麼確保它們按順序執行?
public class Join { public static void main(String[] args) { Thread previous=Thread.currentThread(); for (int i = 0; i < 10; i++) { //每個執行緒擁有前一個執行緒的引用,需要等待前一個執行緒終止,才能從等待中返回 Thread thread=new Thread(new Domino(previous),String.valueOf(i)); thread.start(); previous=thread; } } //靜態內部類 static class Domino implements Runnable{ private Thread thread; public Domino(Thread thread){ this.thread=thread; } public void run(){ try { thread.join(); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" terminate."); } } }
上述程式碼,建立類10個執行緒,編號0~9,每個執行緒呼叫前一個執行緒的join方法,也就是執行緒0結束了,執行緒1才能從join方法中返回,而執行緒0需要等待main執行緒結束。每個執行緒終止的前提是前驅執行緒的終止,每個執行緒等待前驅執行緒終止後,才從join方法返回,本質上仍是等待/通知機制(等待前驅執行緒結束,結束前驅執行緒結束通知)
join方法原始碼如下:
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;
}
}
}
二、ThreadLocal
之前【多執行緒】volatile關鍵字中提及了記憶體模型具有原子性、可見性、有序性。
Thread顧名思義可以理解為執行緒本地變數,是一個以ThreadLocal物件為鍵,任意物件為值的儲存結構。這個結構被附帶線上程上,也就是說一個執行緒可以根據一個ThreadLocal物件查詢到繫結在這個執行緒上的一個值。換句話說就是每個執行緒繫結自己的值。每個執行緒往這個ThreadLocal中讀寫是執行緒隔離的,互相之間不會影響。
通過set(T)方法來設定一個值,在當前執行緒下通過get方法獲取到原先設定的值。
public class Run {
public static ThreadLocal t1=new ThreadLocal();
public static void main(String[] args) {
if (t1.get()==null) {
System.out.println("從未放過值");
t1.set(" 我的值 ");
}
System.out.println(t1.get());
}
}
接下來驗證執行緒的隔離性。public class Tools {
/**
* 驗證執行緒變數的隔離性
*/
public static ThreadLocal t1=new ThreadLocal();
}
public class ThreadA extends Thread {
@Override
public void run(){
try {
for (int i = 0; i < 100; i++) {
Tools.t1.set("ThreadA"+(i+1));
System.out.println("ThreadA get Value="+Tools.t1.get());
Thread.sleep(200);
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
public class ThreadB extends Thread {
@Override
public void run(){
try {
for (int i = 0; i < 100; i++) {
Tools.t1.set("ThreadB"+(i+1));
System.out.println("ThreadB get Value="+Tools.t1.get());
Thread.sleep(200);
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args) {
try {
ThreadA a=new ThreadA();
ThreadB b=new ThreadB();
a.start();
b.start();
for (int i = 0; i < 100; i++) {
Tools.t1.set("Main"+(i+1));
System.out.println("Main get Value="+Tools.t1.get());
Thread.sleep(200);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
雖然3個執行緒都向t1物件中set資料值,但每個執行緒都能取值自己對應的資料。(t1物件是單例的)
ThreadLocal和synchronized都能保證執行緒安全,synchronized是用時間換空間,synchronized是用空間換時間。