Java并发编程(二)如何保证线程同时/交替执行

第一篇文章中,我用如何保证线程顺序执行的例子作为Java并发系列的开胃菜。本篇我们依然不会有源码分析,而是用另外两个多线程的例子来引出Java.util.concurrent中的几个并发工具的用法。

系列文章

Java并发编程(一)如何保证线程顺序执行 - 简书 (jianshu.com)

一、如何保证多个线程同时执行

保证多个线程同时执行,指的是多个线程在同一时间开始执行内部run()方法。

经过第一篇的学习,你应该能理解到,让线程能按我们的意志来运行其实是需要用一些手段(信号量、并发工具、线程池等)来实现的。常用的并发工具一般有CountDownLatch、CyclicBarrier、Semaphore,这些工具在多线程编程中必不可少。我们先看看如何用并发工具保证线程同时执行吧。

1. 使用CountDownLatch实现

关于CountDownLatch,count down的字面意思是倒数,latch是上锁的意思。所以CountDownLatch的意思就是倒数关门。我们看看JDK8 API中是如何解释的:

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

大概意思是,CountDownLatch是一种同步辅助工具,允许一个或多个线程等待一组在其他线程中执行的操作完成之后再执行。

public class SimultaneouslyExample {
static CountDownLatch countDownLatch=new CountDownLatch(3); public static void foo(String name) {
System.out.println("线程名:"+name+",开始时间:"+System.nanoTime());
try {
countDownLatch.await();
//2.每次减一
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
} } public static void main(String[] args) throws InterruptedException{
Thread thread1 = new Thread(() -> foo("A"));
Thread thread2 = new Thread(() -> foo("B"));
Thread thread3 = new Thread(() -> foo("C"));
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(300);
countDownLatch.countDown(); }
}

输出结果:

线程名:A,开始时间:449768159780400

线程名:C,开始时间:449768159785200

线程名:B,开始时间:449768159795300

看到输出结果,你可能会怀疑。明明A线程慢了4800纳秒啊,这不是同步的。其实大可不必觉得奇怪,纳秒级的时间即使是JVM也没办法那么精准的把控,不过根据我的测试。这里的同步实现逻辑能保证毫秒级的精确性。

2. 使用CyclicBarrier实现

另一种实现方式CyclicBarrier,根据字面意思我可以看到这个是一个可循环屏障。CyclicBarrier可以让一个或多个线程到达一个屏障点之后再开始运行。

话不多说,我们直接看看代码中如何写:

public class CyclicBarrierExample{
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(3); public static void foo(String name) {
System.out.println("线程名:"+name+",开始时间:"+System.currentTimeMillis());
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
} } public static void main(String[] args) throws InterruptedException{
Thread thread1 = new Thread(() -> foo("A"));
Thread thread2 = new Thread(() -> foo("B"));
Thread thread3 = new Thread(() -> foo("C"));
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(300); }
}

输出结果:

线程名:A,开始时间:1621232496385

线程名:B,开始时间:1621232496385

线程名:C,开始时间:1621232496385

二、如何保证多个线程交替执行

保证多个线程交替执行,指的是多个线程可以按照一定的次序开始执行内部run()方法。这里我们需要使用Semaphore并发工具来实现。如何你的大学课程学习过操作系统的话,那么你一定对信号量机制很熟悉

Semaphore(信号量):是一种计数器,用来保护一个或者多个共享资源的访问。如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放。

Semaphore的初始化需要传入一个整型参数,此参数标识该信号量可以占用的资源个数。例如我们有两个信号量A,B。A信号量可以允许两个线程占用,B信号量允许一个线程占用,那么初始化的时候Semaphore A = new Semaphore(2);

public class AlternateExample {
private static Semaphore s1 = new Semaphore(1);
private static Semaphore s2 = new Semaphore(1);
private static Semaphore s3 = new Semaphore(1);
static Semaphore[] signals = {s1, s2, s3}; public static void foo(int name) {
while (true) {
try {
signals[name - 1].acquire(); Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名:" + name);
signals[(name) % 3].release();
}
} public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> foo(1));
Thread thread2 = new Thread(() -> foo(2));
Thread thread3 = new Thread(() -> foo(3));
//先占用1和2,此处我们要保证的顺序是3、1、2
s1.acquire();
s2.acquire();
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(300);
}
}

三、总结

本篇我们用两个问题引出了3个并发工具CountDownLatchCyclicBarrierSemaphore的实际应用的例子。下一篇我们讲从源码角度详细分析下这三个工具的实现细节。

参考文章

【完整代码】使用Semaphore实现线程的交替执行打印 A1B2C3D4E5_学亮编程手记-CSDN博客

CountDownLatch详解 - 简书 (jianshu.com)

Java中多个线程交替循环执行 - 坐看云起时_雨宣 - 博客园 (cnblogs.com)

JAVA Semaphore详解 - 简单爱_wxg - 博客园 (cnblogs.com)

上一篇:js for循环中i++ 和 ++i有什么区别?


下一篇:读Java并发编程实践中,向已有线程安全类添加功能--客户端加锁实现示例