多线程顺序打印ABC问题

20 阅读3分钟

问题描述

给定三个线程,分别命名为A、B、C,要求这三个线程按照顺序交替打印ABC,每个字母打印10次,最终输出结果为:ABCABCABCABCABCABCABCABCABCABC。

使用 ReentrantLock 解题

首先分析要素,要有三个线程,多线程打印要做同步,需要有一个锁,多线程实现使用Thread,直接开干。

第一版

非常快速的手写一版框架出来,先运行一下看看效果。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // 分析 思路是分别输出,要保证线程有序输出
        ReentrantLock lock = new ReentrantLock();
        Condition ac = lock.newCondition();
        Condition bc = lock.newCondition();
        Condition cc = lock.newCondition();

        Thread a = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                lock.lock();
                try {
                    System.out.print("A");
                    bc.signal();
                    ac.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread b = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                lock.lock();
                try {
                    System.out.print("B");
                    cc.signal();
                    bc.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread c = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                lock.lock();
                try {
                    System.out.print("C");
                    ac.signal();
                    cc.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        });

        a.start();
        b.start();
        c.start();

        a.join();
        b.join();
        c.join();
    }
}

运行得到结果:

ABCABCABCABCABCABCABCABCABCABCA

貌似没有问题。好像主线程没有退出,一直处于运行状态。

如果调整一下线程的启动顺序再执行呢?

b.start();
a.start();
c.start();

运行得到结果:

BACBACBACBACBACBACBACBACBACBACA

发现打印结果竟然跟启动顺序有关系,这肯定不符合预期!

在多线程场景下,启动的顺序不一定跟执行顺序有关,所以这里要强制介入,保证abc线程的顺序执行。顺便解决主线程未退出的问题。

第二版

执行顺序,可以增加共享变量,通过在线程执行时增加条件判断,来控制输出。

主线程未退出,主要是因为子线程await后没有被唤醒,子线程不退出,所以主线程未退出,通过增加结束条件来解决。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // 分析 思路是分别输出,要保证线程有序输出
        ReentrantLock lock = new ReentrantLock();
        Condition ac = lock.newCondition();
        Condition bc = lock.newCondition();
        Condition cc = lock.newCondition();

        AtomicInteger count = new AtomicInteger(0);
        int max = 30;

        Thread a = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    // 控制当前打印的条件,加的原因是不保证线程的启动顺序就是执行顺序
                    if (count.get() % 3 != 0) {
                        ac.await();
                    }
                    // 保证程序能结束
                    if (count.get() >= max) {
                        bc.signal();
                        return;
                    }
                    System.out.print("A");
                    bc.signal();
                    count.incrementAndGet();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread b = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    if (count.get() % 3 != 1) {
                        bc.await();
                    }
                    if (count.get() >= max) {
                        cc.signal();
                        return;
                    }
                    System.out.print("B");
                    cc.signal();
                    count.incrementAndGet();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread c = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    if (count.get() % 3 != 2) {
                        cc.await();
                    }
                    if (count.get() >= max) {
                        ac.signal();
                        return;
                    }
                    System.out.print("C");
                    ac.signal();
                    count.incrementAndGet();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        });

        b.start();
        a.start();
        c.start();

        a.join();
        b.join();
        c.join();
    }
}

运行得到结果:

ABCABCABCABCABCABCABCABCABCABCA

结果符合预期,线程也能正确关闭。就是代码有些丑,大部分代码的逻辑都是重复的,可以考虑提取优化。

第三版

抽取公共逻辑代码,可拓展至ABCD。。。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // 分析 思路是分别输出,要保证线程有序输出
        ReentrantLock lock = new ReentrantLock();

        AtomicInteger count = new AtomicInteger(0);
        int times = 10; // 循环次数
        int total = 4; // 线程数

        List<Condition> conditions = new ArrayList<>();
        for (int i = 0; i < total; i++) {
            conditions.add(lock.newCondition());
        }

        Thread a = new Thread(new Printer(lock, conditions, count, total * times, 0, total, "A"));
        Thread b = new Thread(new Printer(lock, conditions, count, total * times, 1, total, "B"));
        Thread c = new Thread(new Printer(lock, conditions, count, total * times, 2, total, "C"));
        Thread d = new Thread(new Printer(lock, conditions, count, total * times, 3, total, "D"));

        b.start();
        a.start();
        c.start();
        d.start();

        a.join();
        b.join();
        c.join();
        d.join();
    }

    static class Printer implements Runnable {
        private ReentrantLock lock;
        private List<Condition> conditions;
        private AtomicInteger count;
        private int max;
        private int index;
        private int total;
        private String print;

        public Printer(ReentrantLock lock, List<Condition> conditions, AtomicInteger count, int max, int index, int total, String print) {
            this.lock = lock;
            this.conditions = conditions;
            this.count = count;
            this.max = max;
            this.index = index;
            this.total = total;
            this.print = print;
        }

        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    if (count.get() % total != index) {
                        conditions.get(index).await(); // 当前线程休眠
                    }
                    // 保证程序能结束
                    if (count.get() >= max) {
                        conditions.get((index + 1) % total).signal(); // 唤醒下个线程
                        return;
                    }
                    System.out.print(print);
                    conditions.get((index + 1) % total).signal();
                    count.incrementAndGet();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}