通常情况下,线程的执行顺序都是随机的,哪个获取到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() + "开始执行");
}
}
复制
运行结果:
线程:Thread-1 等待 Thread-0
线程:Thread-5 等待 Thread-4
线程:Thread-4 等待 Thread-3
线程:Thread-3 等待 Thread-2
线程:Thread-2 等待 Thread-1
线程:Thread-0 等待 main
线程:Thread-8 等待 Thread-7
线程:Thread-7 等待 Thread-6
线程:Thread-6 等待 Thread-5
线程:Thread-9 等待 Thread-8
主线程执行完毕
Thread-0开始执行
Thread-1开始执行
Thread-2开始执行
Thread-3开始执行
Thread-4开始执行
Thread-5开始执行
Thread-6开始执行
Thread-7开始执行
Thread-8开始执行
Thread-9开始执行
复制
从执行结果可以很容易理解, 程序运行起来之后,一共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();
}
}
复制
输出,
线程Thread-0开始执行
线程Thread-3开始执行
线程Thread-2开始执行
线程Thread-1开始执行
线程Thread-4开始执行
主线程执行完毕
复制
这种场景在实际项目中有需要的场景,比如我之前看过一个案例,大概的场景是说需要下载一个大文件,开启多个线程分别下载文件的一部分,然后有一个线程最后拼接所有的文件。我们可以考虑使用 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();
}
}
}
复制
输出,
线程Thread-0开始执行
线程Thread-1开始执行
线程Thread-2开始执行
线程Thread-3开始执行
线程Thread-4开始执行
线程Thread-5开始执行
线程Thread-6开始执行
线程Thread-7开始执行
线程Thread-8开始执行
线程Thread-9开始执行
复制
代码也不难理解,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();
}
复制
输出,
线程: thread-0 开始执行
线程: thread-1 开始执行
线程: thread-2 开始执行
线程: thread-3 开始执行
线程: thread-4 开始执行
线程: thread-5 开始执行
线程: thread-6 开始执行
线程: thread-7 开始执行
线程: thread-8 开始执行
线程: thread-9 开始执行
复制
参考
- https://www.baeldung.com/java-countdown-latch