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


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

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

    1. synchronized+wait+notify

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

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

    多线程常见调度方法

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

    那么整体思路也就有了:

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

    ABC-1

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

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

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

    AtomicInteger

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

    Semaphore

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

    CountDownLatch

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

    CyclicBarrier

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

    Exchanger

    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

    ThreadLocal

    • 代码
    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也是相同的姿势

    管道流

    • 代码
    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

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

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

    阻塞队列

    • 代码
    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


    关注下方公众号,领取独家面试资料 ⬇️⬇️⬇️

  • 相关阅读:
    LeetCode43.字符串相乘【大整数相乘】
    LeetCode每日一题(1383. Maximum Performance of a Team)
    音频——I2S 标准模式(二)
    ES6模块化
    EasyRAFT
    【iOS】—— Tagged Pointer对象
    外贸邮件推广怎么统计维度
    MATLAB编程:简易读取分割存储 tif 格式图片
    Kruskal重构树+AC自动机+树状数组:Gym - 104542F
    Spring整合RabbitMQ-配制文件方式-3-消息拉模式
  • 原文地址:https://blog.csdn.net/sinat_40770656/article/details/127120157