轉載自:http://www.machengyu.net/tech/2019/11/15/thread-order.html
敘述
通常情況下,線程的執行順序都是隨機的,哪個獲取到CPU的時間片,哪個就獲得執行的機會。不過實際的項目中有時我們會有需要不同的線程順序執行的需求。藉助一些java中的線程阻塞和同步機制,我們往往也可以控制多個線程的執行順序。
方法有很多種,本篇文章介紹幾種常用的。
解決方案
利用 thread join實現線程順序執行
thread.join方法的可以實現如下的效果,就是掛起調用join方法的線程的執行,直到被調用的線程執行結束。聽起來有點繞,舉個例子解釋下:
假設有t1, t2兩個線程,如果在t2的線程流程中調用了 t1.join
, 那麼t2線程將會停止執行,等待t1執行結束後纔會繼續執行。
很顯然,利用這個機制,我們可以控制線程的執行順序,看下面的例子:
public class ControlThreadDemo {
public static void main(String[] args) {
Thread previousThread = Thread.currentThread();
for (int i = 0; i < 10; i++) {
ThreadJoinDemo threadJoinDemo = new ThreadJoinDemo(previousThread);
threadJoinDemo.start();
previousThread = threadJoinDemo;
}
System.out.println("主線程執行完畢");
}
}
public class ThreadJoinDemo extends Thread{
private Thread previousThread;
public ThreadJoinDemo(Thread thread) {
this.previousThread = thread;
}
public void run() {
try {
System.out.println("線程:" + Thread.currentThread().getName() + " 等待 " + previousThread.getName());
previousThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "開始執行");
}
}
運行結果:
從執行結果可以很容易理解, 程序運行起來之後,一共11個線程排好隊等着執行,排在最前面的是 main 線程,然後依次是t0, t1 …。
利用 CountDownLatch 控制線程的執行順序
還是先說下 CountDownLatch 的用法,CountDownLatch 是一個同步工具類,它允許一個或多個線程一直等待,直到其他線程執行完後再執行。借用一張經典的圖:
CountDownLatch提供兩個核心的方法,countDown和await,後者可以阻塞調用它的線程, 而前者每調用一次,計數器減去1,當計數器減到0的時候,阻塞的線程被喚醒繼續執行。
場景1
先看一個例子,在這個例子中,主線程會等有若干個子線程執行完畢之後再執行,不過這若干個子線程之間的執行順序是隨機的。
public class ControlThreadDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
List<Thread> workers = Stream.generate(() -> new Thread(new CountDownDemo(countDownLatch))).limit(5).collect(Collectors.toList());
workers.forEach(Thread::start);
countDownLatch.await();
System.out.println("主線程執行完畢");
}
}
public class CountDownDemo implements Runnable{
private CountDownLatch countDownLatch;
public CountDownDemo(CountDownLatch latch) {
this.countDownLatch = latch;
}
@Override
public void run() {
System.out.println("線程" + Thread.currentThread().getName() + "開始執行");
countDownLatch.countDown();
}
}
輸出,
這種場景在實際項目中有需要的場景,比如我之前看過一個案例,大概的場景是說需要下載一個大文件,開啓多個線程分別下載文件的一部分,然後有一個線程最後拼接所有的文件。我們可以考慮使用 CountDownLatch 來控制併發,使拼接的線程放在最後執行。
場景2
這個案例帶你瞭解下利用 CountDownLatch 控制一組線程一起執行。就好像在運動場上,教練的發令槍一響,所有運動員一起跑。我們一般在模擬線程併發執行的時候會用到這種場景。
我們把場景1的代碼稍微改造一下,
public class ControlThreadDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch readyLatch = new CountDownLatch(5);
CountDownLatch runningLatchWait = new CountDownLatch(1);
CountDownLatch completeLatch = new CountDownLatch(5);
List<Thread> workers = Stream.generate(() -> new Thread(new CountDownDemo2(readyLatch,runningLatchWait,completeLatch))).limit(5).collect(Collectors.toList());
workers.forEach(Thread::start);
readyLatch.await();//等待發令
runningLatchWait.countDown();//發令
completeLatch.await();//等所有子線程執行完
System.out.println("主線程執行完畢");
}
}
public class CountDownDemo2 implements Runnable{
private CountDownLatch readyLatch;
private CountDownLatch runningLatchWait;
private CountDownLatch completeLatch;
public CountDownDemo2(CountDownLatch readyLatch, CountDownLatch runningLatchWait, CountDownLatch completeLatch) {
this.readyLatch = readyLatch;
this.runningLatchWait = runningLatchWait;
this.completeLatch = completeLatch;
}
@Override
public void run() {
readyLatch.countDown();
try {
runningLatchWait.await();
System.out.println("線程" + Thread.currentThread().getName() + "開始執行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
completeLatch.countDown();
}
}
}
場景3
到這裏,可能很多人會想問,利用 CountDownLatch 能做到像前面thread.join控制多個線程按照一個固定的先後順序執行嗎?
首先我要說,用 CountDownLatch 實現這種場景確實不多見,不過也不是不可以做。請繼續看場景3。
public class ControlThreadDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch first = new CountDownLatch(1);
CountDownLatch prev = first;
for (int i = 0; i < 10; i++) {
CountDownLatch next = new CountDownLatch(1);
new CountDownDemo3(prev, next).start();
prev = next;
}
first.countDown();
}
}
public class CountDownDemo3 extends Thread{
private CountDownLatch prev;
private CountDownLatch next;
public CountDownDemo3(CountDownLatch prev, CountDownLatch next) {
this.prev = prev;
this.next = next;
}
@Override
public void run() {
try {
prev.await();
Thread.sleep(1000);//模擬線程執行耗時
System.out.println("線程" + Thread.currentThread().getName() + "開始執行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
next.countDown();
}
}
}
輸出,
代碼也不難理解,for循環裏把10個線程串聯起來,排好隊等着執行。排在最前面的線程t1在等first這個計數器countDown,然後t1開始執行,執行完調用自己的next計數器 countDown 以喚醒下一個,依次類推。
利用 newSingleThreadExecutor 控制線程的執行順序
java的 Executors 線程池平時工作中用得很多了,JAVA通過Executors提供了四種線程池,單線程化線程池(newSingleThreadExecutor)、可控最大併發數線程池(newFixedThreadPool)、可回收緩存線程池(newCachedThreadPool)、支持定時與週期性任務的線程池(newScheduledThreadPool)。
顧名思義,newSingleThreadExecutor 線程池只有一個線程。它存在的意義就在於控制線程執行的順序,保證任務的執行順序和提交順序一致。其實保證順序執行的原理也很簡單,因爲總是隻有一個線程處理任務隊列上的任務,先提交的任務必將被先處理。
廢話不多說,上代碼。
public static void main(String[] args) throws InterruptedException {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++){
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName("thread-" + index);
System.out.println("線程: " + Thread.currentThread().getName() + " 開始執行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
executorService.awaitTermination(30, TimeUnit.SECONDS);
executorService.shutdownNow();
}
輸出,