Java 控制線程執行順序的幾種方法

轉載自: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();
    }

輸出,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章