三个线程按顺序打印ABC?十二种做法,深入多线程同步通信机制

本文涉及的产品
云原生网关 MSE Higress,422元/月
可观测监控 Prometheus 版,每月50GB免费额度
应用实时监控服务-应用监控,每月50GB免费额度
简介: 大家好,我是老三,这篇文章分享一道非常不错的题目:三个线程按序打印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次

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

相关文章
|
3天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
12 1
|
29天前
|
存储 监控 安全
深入理解ThreadLocal:线程局部变量的机制与应用
在Java的多线程编程中,`ThreadLocal`变量提供了一种线程安全的解决方案,允许每个线程拥有自己的变量副本,从而避免了线程间的数据竞争。本文将深入探讨`ThreadLocal`的工作原理、使用方法以及在实际开发中的应用场景。
55 2
|
2月前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
42 1
[Java]线程生命周期与线程通信
|
1月前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
39 3
|
1月前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
88 2
|
1月前
|
Java 调度
Java 线程同步的四种方式,最全详解,建议收藏!
本文详细解析了Java线程同步的四种方式:synchronized关键字、ReentrantLock、原子变量和ThreadLocal,通过实例代码和对比分析,帮助你深入理解线程同步机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 线程同步的四种方式,最全详解,建议收藏!
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
25 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
41 2
|
1月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
66 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
60 1