首页 > 其他分享 >三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制

时间:2022-10-09 16:39:03浏览次数:62  
标签:ABC lock state static println 线程 多线程 public


大家好,我是老三,这篇文章分享一道非常不错的题目:三个线程按序打印ABC。

很多读者朋友应该都觉得这道题目不难,这次给大家带来十二种做法,一定有你没有见过的新姿势。

1. synchronized+wait+notify

说到同步,我们很容易就想到synchronized。

线程间通信呢?我们先回忆一下线程间的调度。

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_多线程

可以看到,等待和运行之间的转换可以用wait和notify。

那么整体思路也就有了:

  • 打印的时候需要获取锁
  • 打印B的线程需要等待打印A线程执行完,打印C的线程需要等待打印B线程执行完

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_算法_02

  • 代码
public class ABC1 {
//锁住的对象
private final static Object lock = new Object();
//A是否已经执行
private static boolean aExecuted = false;
//B是否已经执行过
private static boolean bExecuted = false;

public static void printA() {
synchronized (lock) {
System.out.println("A");
aExecuted = true;
//唤醒所有等待线程
lock.notifyAll();
}
}

public static void printB() throws InterruptedException {
synchronized (lock) {
//获取到锁,但是要等A执行
while (!aExecuted) {
lock.wait();
}
System.out.println("B");
bExecuted = true;
lock.notifyAll();
}
}

public static void printC() throws InterruptedException {
synchronized (lock) {
//获取到锁,但是要等B执行
while (!bExecuted) {
lock.wait();
}
System.out.println("C");
}
}

}
  • 测试:后面几种方法的单测基本和这种方法一致,所以后面的单测就省略了。
@Test
void abc1() {
//线程A
new Thread(() -> {
ABC1.printA();
}, "A").start();
//线程B
new Thread(() -> {
try {
ABC1.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
//线程C
new Thread(() -> {
try {
ABC1.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "C").start();
}

2. lock+全局变量state

还可以用lock+state来实现,大概思路:

  • 用lock来实现同步
  • 用全局变量state标识改哪个线程执行,不执行就释放锁

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_jvm_03

  • 代码
public class ABC2 {
//可重入锁
private final static Lock lock = new ReentrantLock();
//判断是否执行:1表示应该A执行,2表示应该B执行,3表示应该C执行
private static int state = 1;

public static void printA() {
//自旋
while (state < 4) {
try {
//获取锁
lock.lock();
//并发情况下,不能用if,要用循环判断等待条件,避免虚假唤醒
while (state == 1) {
System.out.println("A");
state++;
}
} finally {
//要保证不执行的时候,锁能释放掉
lock.unlock();
}
}
}

public static void printB() throws InterruptedException {
while (state < 4) {
try {
lock.lock();
//获取到锁,应该执行
while (state == 2) {
System.out.println("B");
state++;
}
} finally {
lock.unlock();
}
}
}

public static void printC() throws InterruptedException {
while (state < 4) {
try {
lock.lock();
while (state == 3) {
//获取到锁,应该执行
System.out.println("C");
state++;
}
} finally {
lock.unlock();
}
}
}

}

这里也有几个细节要注意:

  • 要在循环里获取锁,不然线程可能会在获取到锁之前就终止了
  • 要用while,而不是if判断,是否当前线程应该打印输出
  • 要在finally里释放锁,保证其它的线程能获取到锁

3. volatile

上一种做法,我们用了同步+全局变量的方式,那么有没有什么更轻量级的做法?

我们可以直接用volatile修饰变量,volatile能保证变量的更改对所有线程可见。

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_jvm_04

  • 代码
public class ABC3 {

//判断是否执行:1表示应该A执行,2表示应该B执行,3表示应该C执行
private static volatile Integer state = 1;

public static void printA() {
//通过循环,hang住线程
while (state != 1) {
}
System.out.println("A");
state++;
}

public static void printB() throws InterruptedException {
while (state != 2) {
}
System.out.println("B");
state++;
}

public static void printC() throws InterruptedException {
while (state != 3) {
}
System.out.println("C");
state++;
}

}

4. AtomicInteger

除了无锁的volatile方法,还有没有什么轻量级锁的方法呢?

我们都知道synchronized和lock都属于悲观锁,我们还可以用乐观锁来实现。

在Java里,我们熟悉的原子操作类AtomicInteger就是基于CAS实现的,可以用来保证Integer操作的原子性。

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_java_05

  • 代码
public class ABC4 {

//判断是否执行:1表示应该A执行,2表示应该B执行,3表示应该C执行
private static AtomicInteger state = new AtomicInteger(1);

public static void printA() {
System.out.println("A");
state.incrementAndGet();
}

public static void printB() throws InterruptedException {
while (state.get() < 4) {
while (state.get() == 2) {
System.out.println("B");
state.incrementAndGet();
}
}
}

public static void printC() throws InterruptedException {
while (state.get() < 4) {
while (state.get() == 3) {
System.out.println("C");
state.incrementAndGet();
}
}
}

}

5.lock+condition

在Java中,除了Object的waitnotify/notify可以实现等待/通知机制,ConditionLock配合同样可以完成等待通知机制。

使用​​condition.await()​​​,使当前线程进入等待状态,使用​​condition.signal()​​​或者​​condition.signalAll()​​唤醒等待线程。

  • 代码
public class ABC5 {
//可重入锁
private final static Lock lock = new ReentrantLock();
//判断是否执行:1表示应该A执行,2表示应该B执行,3表示应该C执行
private static int state = 1;
//condition对象
private static Condition a = lock.newCondition();
private static Condition b = lock.newCondition();
private static Condition c = lock.newCondition();

public static void printA() {
//通过循环,hang住线程
while (state < 4) {
try {
//获取锁
lock.lock();
//并发情况下,不能用if,要用循环判断等待条件,避免虚假唤醒
while (state != 1) {
a.await();
}
System.out.println("A");
state++;
b.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//要保证不执行的时候,锁能释放掉
lock.unlock();
}
}
}

public static void printB() throws InterruptedException {
while (state < 4) {
try {
lock.lock();
//获取到锁,应该执行
while (state != 2) {
b.await();
}
System.out.println("B");
state++;
c.signal();
} finally {
lock.unlock();
}
}
}

public static void printC() throws InterruptedException {
while (state < 4) {
try {
lock.lock();
while (state != 3) {
c.await();
}
//获取到锁,应该执行
System.out.println("C");
state++;
} finally {
lock.unlock();
}
}
}

}

6.信号量Semaphore

线程间同步,还可以使用信号量Semaphore,信号量顾名思义,多线程协作时完成信号传递。

使用acquire()获取许可,如果没有可用的许可,线程进入阻塞等待状态;使用release释放许可。

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_多线程_06

  • 代码
public class ABC6 {

private static Semaphore semaphoreB = new Semaphore(0);
private static Semaphore semaphoreC = new Semaphore(0);

public static void printA() {
System.out.println("A");
semaphoreB.release();
}

public static void printB() throws InterruptedException {
semaphoreB.acquire();
System.out.println("B");
semaphoreC.release();
}

public static void printC() throws InterruptedException {
semaphoreC.acquire();
System.out.println("C");
}

}

7.计数器CountDownLatch

CountDownLatch的一个适用场景,就是用来进行多个线程的同步管理,线程调用了countDownLatch.await()之后,需要等待countDownLatch的信号countDownLatch.countDown(),在收到信号前,它不会往下执行。

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_并发_07

public class ABC7 {

private static CountDownLatch countDownLatchB = new CountDownLatch(1);
private static CountDownLatch countDownLatchC = new CountDownLatch(1);

public static void printA() {
System.out.println("A");
countDownLatchB.countDown();
}

public static void printB() throws InterruptedException {
countDownLatchB.await();
System.out.println("B");
countDownLatchC.countDown();
}

public static void printC() throws InterruptedException {
countDownLatchC.await();
System.out.println("C");
}

}

8. 循环栅栏CyclicBarrier

用到了​​CountDownLatch​​​,我们应该想到,还有一个功能和它类似的工具类​​CyclicBarrier​​。

有的翻译叫同步屏障,我觉得翻译成循环栅栏,更能体现它的功能特性。

就像是出去旅游,大家不同时间到了景区门口,但是景区疫情限流,先把栅栏拉下来,在景区里的游客走一批,打开栅栏,再放进去一批,走一批,再放进去一批……

这就是CyclicBarrier的两个特性,

  • 栅栏:多个线程相互等待,到齐后再执行特定动作
  • 循环:所有线程释放后,还能继续复用它

这道题怎么用CyclicBarrier解决呢?

  • 线程B和线程C需要使用栅栏等待
  • 为了让B和C也顺序执行,需要用一个状态,来标识应该执行的线程

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_jvm_08

  • 代码
public class ABC8 {

private static CyclicBarrier cyclicBarrier = new CyclicBarrier(1);
private static Integer state = 1;

public static void printA() {
while (state != 1) {
}
System.out.println("A");
state = 2;
}

public static void printB() throws InterruptedException {
try {
//在栅栏前等待
cyclicBarrier.await();
//state不等于2的时候等待
while (state != 2) {
}
System.out.println("B");
state = 3;
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}

public static void printC() throws InterruptedException {
try {
cyclicBarrier.await();
while (state != 3) {
}
System.out.println("C");
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}

}

当然,CyclicBarrier的实现其实还是基于lock+condition,多个线程在到达一定条件前await,到达条件后signalAll。

9.交换器Exchanger

在前面,我们已经用到了常用的并发工具类,其实还有一个不那么常用的并发工具类​​Exchanger​​,同样也可以用来解决这道题目。

Exchanger用于两个线程在某个节点时进行数据交换,在这道题里:

  • 线程A执行完之后,和线程B用一个交换器交换state,线程B执行完之后,和线程C用一个交换器交换state
  • 在没有轮到自己执行之前,先进行等待

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_多线程_09

public class ABC9 {
private static Exchanger<Integer> exchangerB = new Exchanger<>();
private static Exchanger<Integer> exchangerC = new Exchanger<>();

public static void printA() {
System.out.println("A");
try {
//交换
exchangerB.exchange(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void printB() {
try {
//交换
Integer state = exchangerB.exchange(0);
//等待
while (state != 2) {
}
//执行
System.out.println("B");
//第二次交换
exchangerC.exchange(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void printC() {
try {
Integer state = exchangerC.exchange(0);
while (state != 3) {
}
System.out.println("C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Exchanger是基于ThreadLocal实现的,那么我们这个问题可以基于ThreadLocal来实现吗?

10.ThreadLocal

ThreadLocal,我们应该都了解过它的用法和原理,那么怎么用ThreadLocal实现三个线程顺序打印ABC呢?

子线程是并发执行的,但是主线程的代码是顺序执行的,我们在主线程里改变变量,子线程根据变量判断。

那么问题来了,子线程怎么获取主线程的变量呢?可以用​​InheritableThreadLocal​​。

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_并发_10

  • 代码
public class ABC10 {

public static void main(String[] args) {
//使用ThreadLocal存储变量
ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
threadLocal.set(1);
new Thread(() -> {
System.out.println("A");
}, "A").start();
//设置变量值
threadLocal.set(2);

new Thread(() -> {
//等待
while (threadLocal.get() != 2) {
}
System.out.println("B");
}, "B").start();
threadLocal.set(3);

new Thread(() -> {
while (threadLocal.get() != 3) {
}
System.out.println("C");
}, "C").start();
}
}

11.管道流PipedStream

线程之间通信,还有一种比较笨重的办法——PipedInputStream/PipedOutStream。

一个线程使用PipedOutStream写数据,一个线程使用PipedInputStream读数据,而且Piped的读取只能一对一。

那么,在这道题里:

  • 线程A使用PipedOutStream向线程B写入数据,线程B读取后,打印输出
  • 线程B和C也是相同的姿势

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_并发_11

  • 代码
public class ABC11 {
public static void main(String[] args) throws IOException {
//线程A的输出流
PipedOutputStream outputStreamA = new PipedOutputStream();
//线程B的输出流
PipedOutputStream outputStreamB = new PipedOutputStream();
//线程B的输入流
PipedInputStream inputStreamB = new PipedInputStream();
//线程C的输入流
PipedInputStream inputStreamC = new PipedInputStream();


outputStreamA.connect(inputStreamB);
outputStreamB.connect(inputStreamC);

new Thread(() -> {
System.out.println("A");
try {
//流写入
outputStreamA.write("B".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}, "A").start();

new Thread(() -> {
//流读取
byte[] buffer = new byte[1];
try {
inputStreamB.read(buffer);
//转换成String
String msg = new String(buffer);
System.out.println(msg);
outputStreamB.write("C".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}, "B").start();

new Thread(() -> {
byte[] buffer = new byte[1];
try {
inputStreamC.read(buffer);
String msg = new String(buffer);
System.out.println(msg);
} catch (IOException e) {
e.printStackTrace();
}
}, "C").start();
}
}

12.阻塞队列BlockingQueue

阻塞队列同样也可以用来进行线程调度。

  • 利用队列的长度,来确定执行者
  • 利用队列的阻塞性,来保证入队操作同步执行。

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制_java_12

  • 代码
public class ABC12 {

private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

public static void printA() {
System.out.println("A");
queue.offer("B");
}

public static void printB() throws InterruptedException {
while (queue.size() != 1) {
}
System.out.println("B");
queue.offer("C");
}

public static void printC() throws InterruptedException {
while (queue.size() != 2) {
}
System.out.println("C");
}

}

总结

这篇文章给大家带来了​​三个线程顺序打印ABC的​​的十二种做法,里面有些写法肯定是冗余的,大家有没有什么更好的写法呢?

通过十二种题解,我们基本上把Java并发中主要的线程同步和通信方式过了一遍,相信通过这道题的实践,我们也能对Java线程的同步和通信有更深的理解。

最后,也给大家留两道“进阶”一点的题目,感兴趣可以自己实现一下:

  • 两个线程,一个线程打印奇数,一个线程打印偶数
  • 按照顺序,三个线程分别打印A5次,B10次,C15次



参考:

[1]. https://zhuanlan.zhihu.com/p/368409843

[2].http://edisonxu.com/2017/03/02/java-thread-communication.html

[3].https://redspider.gitbook.io/concurrent/di-yi-pian-ji-chu-pian/5



标签:ABC,lock,state,static,println,线程,多线程,public
From: https://blog.51cto.com/u_15463439/5740688

相关文章

  • Java 多线程(三)静态代理模式
    静态代理模式:1.真实角色和代理角色实现同一个接口2.代理角色要代理真实角色3.代理角色可以做真实角色做不了的事4.真实角色专注做自己的事publicclassStaticProxy......
  • 达梦数据库体系结构(物理结构、逻辑结构、内存结构、线程结构)
    DM目录数据库安装目录下图展示为DM8数据库目录。  /dm8/bin 目录存放DM数据库的可执行文件,例如disql命令、dminit命令、dmrman工具等。  /dm8/deskto......
  • Java实现多线程
    Java实现多线程的方式有4种分别是继承Thread类,实现Runnable,Callable接口和通过线程池提交线程任务。其中实现Callable接口的方式可以获取返回值。1.继承Thread类通过继......
  • 多线程问题sleep与wait
    涉及到的三个方法:wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高......
  • 多线程篇4:工具类
    一、Semaphore、Exchanger、CountDownLatch、CyclicBarrier、PhaserJDK中提供了⼀些线程通信⼯具类以供开发者使⽤。这样的话我们在遇到⼀些常⻅的应⽤场景时就可以使⽤这......
  • Java多线程(二)线程创建
    三种创建方式1.继承Thread类2.实现Runnable接口3.实现Callable接口(了解) 继承Thread类自定义线程继承Thread类重写run()方法,编写线程执行体创建线程对象,调用start(......
  • 手写线程池
     我们先不去看线程池原理,然后自己一步一步的分析,看看线程池都需要做哪些工作。 然后再一步一步的实现它,然后再去对比比人写好的线程池,然后看看差距在哪里。 ## 先分析......
  • abc272_f Two Strings (后缀数组)
    https://atcoder.jp/contests/abc272/tasks/abc272_f将SS#TT在字符串中排序,看标号为1-n后面有多少2n+2-3n+1的标号然后就会注意题目要的是小于等于,那么要拼成SS......
  • Java 多线程(一)线程简介
    多任务类似于这些例子,现实生活中太多太多了。看起来是多个任务在做,其实本质上我们的大脑再同一时间依旧只做一件事。 多线程  原来是一条路,慢慢的因为车多起来......
  • 线程池
    池化技术线程属于稀缺资源,由于创建线程和销毁线程十分消耗内存和资源,因此实现线程的复用十分重要将创建的线程存入线程池管理,实现线程的复用,提高了cpu利用率池化技术,比......