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

Posted 三分恶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制相关的知识,希望对你有一定的参考价值。

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

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

1. synchronized+wait+notify

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

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

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

那么整体思路也就有了:

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

  • 代码
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标识改哪个线程执行,不执行就释放锁

  • 代码
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能保证变量的更改对所有线程可见。

  • 代码
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操作的原子性。

  • 代码
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释放许可。

  • 代码
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(),在收到信号前,它不会往下执行。

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也顺序执行,需要用一个状态,来标识应该执行的线程

  • 代码
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
  • 在没有轮到自己执行之前,先进行等待

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() 
        

以上是关于三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制的主要内容,如果未能解决你的问题,请参考以下文章

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

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

多线程面试题之三线程按顺序交替打印ABC的方法

java多线程:结合多线程交替打印10次abc实例,对wait/notify使用的彻底理解

java多线程编程之连续打印abc的几种解法

[******] java多线程连续打印abc