記一次主線程等待子線程結束的多種方法的學習
在學習多線程時,最開始遇到的問題其實是“計算子線程運行時間”,寫到最后發現本文和標題更為符合,但是仍然基於問題:“在主線程中獲取子線程的運行時間”。
while循環
對於“主線程如何獲取子線程總運行時間”的問題,最開始想到的是使用while循環進行輪詢:
Thread t = new Thread(() -> {
//子線程進行字符串連接操作
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
});
//開始計時
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t.start();
long end = 0;
while(t.isAlive() == true){//t.getState() != State.TERMINATED這兩種判斷方式都可以
end = System.currentTimeMillis();
}
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
但是這樣太消耗CPU,所以我在while循環里加入了暫停:
while(t.isAlive() == true){
end = System.currentTimeMillis();
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
}
這樣做的結果雖然cpu消耗減少,但是數據不准確了
Thread的join()方法
接着我又找到了第二種方法:
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t1.start();
try {
t.join();//注意這里
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - Start:" + (end - start));
使用join()方法,join()方法的作用,是等待這個線程結束;(t.join()方法阻塞調用此方法的線程(calling thread),直到線程t完成,此線程再繼續,這里貼個說的挺清楚的博客)
synchronized的等待喚醒機制
第二種方法的確實現了計時,接着我又想到了多線程的等待喚醒機制,思路是:子線程啟動后主線程等待,子線程結束后喚醒主線程。於是有了下面的代碼:
Object lock = new Object();
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
lock.notify();//子線程喚醒
});
//計時
long start = System.currentTimeMillis();
System.out.println("start = " + start);
//啟動子線程
t.start();
try {
lock.wait();//主線程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
但是這樣會拋出兩個異常:
由於對wait()和notify()的理解並不是很深刻,所以我最開始並不清楚為什么會出現這樣的結果,因為從報錯順序來看子線程並沒有提前喚醒,於是我在segmentfault和CSDN都發出了提問,同時也詢問了我一個很厲害的朋友,最后得出的結論是調用wait()方法時需要獲取該對象的鎖,Object文檔里是這么說的:
The current thread must own this object's monitor.
IllegalMonitorStateException - if the current thread is not the owner of the object's monitor.
所以上面的代碼需要改成這樣:
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
synchronized (lock) {//獲取對象鎖
lock.notify();//子線程喚醒
}
});
//計時
long start = System.currentTimeMillis();
System.out.println("start = " + start);
//啟動子線程
t.start();
try {
synchronized (lock) {//這里也是一樣
lock.wait();//主線程等待
}
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
這樣的確得出了結果,但是我想知道兩個線程的執行順序,於是在wait和nitify前后分別加了一個輸出,最后得出的運行結果是:
可以看出主線程先wait子線程再notify,也就是說,如果子線程在主線程wati前調用了nitify,會導致主線程無限等待,所以這個思路還是有一定的漏洞的。
關於wait和notify這里貼個挺清楚的博客
CountDownLatch
第四種方式可以等待多個線程結束,就是使用java.util.concurrent包下的CountDownLatch類(關於CountDownLatch的用法可以參考這篇簡潔的博客)
簡單來說,CountDownLatch類是一個計數器,可以設置初始線程數(設置后不能改變),在子線程結束時調用countDown()方法可以使線程數減一,最終為0的時候,調用CountDownLatch的成員方法wait()的線程就會取消BLOKED阻塞狀態,進入RUNNABLE從而繼續執行。下面上代碼:
int threadNumber = 1;
final CountDownLatch cdl = new CountDownLatch(threadNumber);//參數為線程個數
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
cdl.countDown();//此方法是CountDownLatch的線程數-1
});
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t.start();
//線程啟動后調用countDownLatch方法
try {
cdl.await();//需要捕獲異常,當其中線程數為0時這里才會繼續運行
}catch (InterruptedException e){
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
Future
又想到剛學習了線程池,線程池的submit()的返回對象Future接口有一個get()方法也可以阻塞當前線程(其實該方法主要用途是獲取子線程的返回值),所以第五種方法也出來了:
ExecutorService executorService = Executors.newFixedThreadPool(1);
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
});
long start = System.currentTimeMillis();
System.out.println("start = " + start);
Future future = executorService.submit(t);//子線程啟動
try {
future.get();//需要捕獲兩種異常
}catch (InterruptedException e){
e.printStackTrace();
}catch (ExecutionException e){
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
executorService.shutdown();
這里, ThreadPoolExecutor 是實現了 ExecutorService的方法, sumbit的過程就是把一個Runnable接口對象包裝成一個 Callable接口對象, 然后放到 workQueue里等待調度執行. 當然, 執行的啟動也是調用了thread的start來做到的, 只不過這里被包裝掉了. 另外, 這里的thread是會被重復利用的, 所以這里要退出主線程, 需要執行以下shutdown方法以示退出使用線程池. 扯遠了.
這種方法是得益於Callable接口和Future模式, 調用future接口的get方法, 會同步等待該future執行結束, 然后獲取到結果. Callbale接口的接口方法是 V call(); 是可以有返回結果的, 而Runnable的 void run(), 是沒有返回結果的. 所以, 這里即使被包裝成Callbale接口, future.get返回的結果也是null的.如果需要得到返回結果, 建議使用Callable接口.參見這篇博客
看到這個Callable突然想到之前看C#多線程的時候有說到回調的問題,因此先開個坑,下篇博文說說Java的Callable與callback問題,先貼個Callable的簡單講解
BlockingQueue
同時,在concurrent包中,還提供了BlockingQueue(隊列)來操作線程,BlockingQueue的主要的用法是在線程間安全有效的傳遞數據,具體用法可以參見這篇博客,對於BlockingQueue說的非常詳細。因此,第六種方法也出來了:
BlockingQueue queue = new ArrayBlockingQueue(1);//數組型隊列,長度為1
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
try {
queue.put("OK");//在隊列中加入數據
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t.start();
try {
queue.take();//主線程在隊列中獲取數據,take()方法會阻塞隊列,ps還有不會阻塞的方法
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
CyclicBarrier
那么,有沒有第七種方式呢?當然有啦~,還是concurrent包,只不過這次試用CyclicBarrier類:
CyclicBarrier字面意思回環柵欄,通過它可以實現讓一組線程等待至某個狀態之后再全部同時執行。叫做回環是因為當所有等待線程都被釋放以后,CyclicBarrier可以被重用。
CyclicBarrier barrier = new CyclicBarrier(2);//參數為線程數
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
try {
barrier.await();//阻塞
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t.start();
try {
barrier.await();//也阻塞,並且當阻塞數量達到指定數目時同時釋放
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
實際是上面這種方法是不太嚴謹的,因為在子線程阻塞之后如果還有代碼是會繼續執行的,當然本例中后面是沒有代碼可執行了,可以近似理解為是子線程的運行時間。
這里貼個CountDownLatch、CyclicBarrier和Semaphore的講解博客
小結
至此,集齊了七顆龍珠,得出小結:
- while循環進行輪詢
- Thread類的join方法
- synchronized鎖
- CountDownLatch
- Future
- BlockingQueue
- CyclicBarrier