华为OD面试:三个线程交替打印ABC如何实现?

科技   2025-01-05 17:02   江苏  
将 脚本之家 设为“星标
第一时间收到文章更新

本文经JavaGuide(id:JavaGuide)授权转载

你好,我是 Guide。看到两道非常有意思的多线程手撕题,分享一下解题思路:

  1. 三个线程交替打印 ABC
  2. 控制三个线程的执行顺序

三个线程交替打印 ABC

问题描述:写三个线程打印 "ABC",一个线程打印 A,一个线程打印 B,一个线程打印 C,一共打印 10 轮。

这里提供一个 Semaphore版本和 ReentrantLock + Condition 版本。

Semaphore 实现

我们先定义一个类 ABCPrinter 用于实现三个线程交替打印 ABC。

public class ABCPrinter {
    private final int max;
    // 从线程 A 开始执行
    private final Semaphore semaphoreA = new Semaphore(1);
    private final Semaphore semaphoreB = new Semaphore(0);
    private final Semaphore semaphoreC = new Semaphore(0);

    public ABCPrinter(int max) {
        this.max = max;
    }

    public void printA() {
        print("A", semaphoreA, semaphoreB);
    }

    public void printB() {
        print("B", semaphoreB, semaphoreC);
    }

    public void printC() {
        print("C", semaphoreC, semaphoreA);
    }

    private void print(String alphabet, Semaphore currentSemaphore, Semaphore nextSemaphore) {
        for (int i = 1; i <= max; i++) {
            try {
                currentSemaphore.acquire();
                System.out.println(Thread.currentThread().getName() + " : " + alphabet);
                // 传递信号给下一个线程
                nextSemaphore.release();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
    }
}

可以看到,我们这里用到了三个信号量,分别用于控制这三个线程的交替执行。semaphoreA 信号量先获取,也就是先输出“A”。一个线程执行完之后,就释放下一个信号量。也就是,A 线程执行完之后释放semaphoreB信号量,B 线程执行完之后释放semaphoreC信号量,以此类推。

接着,我们创建三个线程,分别用于打印 ABC。

ABCPrinter printer = new ABCPrinter(10);
Thread t1 = new Thread(printer::printA, "Thread A");
Thread t2 = new Thread(printer::printB, "Thread B");
Thread t3 = new Thread(printer::printC, "Thread C");

t1.start();
t2.start();
t3.start();

输出如下:

Thread A : A
Thread B : B
Thread C : C
......
Thread A : A
Thread B : B
Thread C : C

ReentrantLock + Condition 实现

思路和 synchronized+wait/notify 很像。

public class ABCPrinter {
    private final int max;
    // 用来指示当前应该打印的线程序号,0-A, 1-B, 2-C
    private int turn = 0;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition conditionA = lock.newCondition();
    private final Condition conditionB = lock.newCondition();
    private final Condition conditionC = lock.newCondition();

    public ABCPrinter(int max) {
        this.max = max;
    }

    public void printA() {
        print("A", conditionA, conditionB);
    }

    public void printB() {
        print("B", conditionB, conditionC);
    }

    public void printC() {
        print("C", conditionC, conditionA);
    }

    private void print(String name, Condition currentCondition, Condition nextCondition) {
        for (int i = 0; i < max; i++) {
            lock.lock();
            try {
                // 等待直到轮到当前线程打印
                // turn 变量的值需要与线程要打印的字符相对应,例如,如果turn是0,且当前线程应该打印"A",则条件满足。如果不满足,当前线程调用currentCondition.await()进入等待状态。
                while (!((turn == 0 && name.charAt(0) == 'A') || (turn == 1 && name.charAt(0) == 'B') || (turn == 2 && name.charAt(0) == 'C'))) {
                    currentCondition.await();
                }
                System.out.println(Thread.currentThread().getName() + " : " + name);
                // 更新打印轮次,并唤醒下一个线程
                turn = (turn + 1) % 3;
                nextCondition.signal();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }
    }
}

在上面的代码中,三个线程的协调主要依赖:

  • ReentrantLock lock: 用于线程同步的可重入锁,确保同一时刻只有一个线程能修改共享资源。
  • Condition conditionA/B/C: 分别与"A"、"B"、"C"线程关联的条件变量,用于线程间的协调通信。一个线程执行完之后,通过调用nextCondition.signal()唤醒下一个应该打印的线程。

控制三个线程的执行顺序

问题描述:假设有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?

这道题不难,网上大部分人都是用 join()或者 CountDownLatch 实现,但个人更推荐CompletableFuture

代码如下(这里为了简化代码,用到了 Hutool 的线程工具类 ThreadUtil 和日期时间工具类 DateUtil):

// T1
CompletableFuture<Void> futureT1 = CompletableFuture.runAsync(() -> {
    System.out.println("T1 is executing.Current time:" + DateUtil.now());
    // 模拟耗时操作
    ThreadUtil.sleep(1000);
});

// T2 在 T1 完成后执行
CompletableFuture<Void> futureT2 = futureT1.thenRunAsync(() -> {
    System.out.println("T2 is executing after T1.Current time:" + DateUtil.now());
    ThreadUtil.sleep(1000);
});

// T3 在 T2 完成后执行
CompletableFuture<Void> futureT3 = futureT2.thenRunAsync(() -> {
    System.out.println("T3 is executing after T2.Current time:" + DateUtil.now());
    ThreadUtil.sleep(1000);
});

// 等待所有任务完成,验证效果
ThreadUtil.sleep(3000);

可以看到,我们这里通过 thenRunAsync()方法就实现了 T1、T2、T3 的顺序执行。thenRunAsync()方法的作用就是做完第一个任务后,再做第二个任务。也就是说某个任务执行完成后,执行回调方法

输出:

T1 is executing.Current time:2024-06-23 21:59:38
T2 is executing after T1.Current time:2024-06-23 21:59:39
T3 is executing after T2.Current time:2024-06-23 21:59:40

如果我们想要实现 T3 在 T2 和 T1 执行完后执行,T2 和 T1 可以同时执行,应该怎么办呢?

// T1
CompletableFuture<Void> futureT1 = CompletableFuture.runAsync(() -> {
    System.out.println("T1 is executing. Current time:" + DateUtil.now());
    // 模拟耗时操作
    ThreadUtil.sleep(1000);
});
// T2
CompletableFuture<Void> futureT2 = CompletableFuture.runAsync(() -> {
    System.out.println("T2 is executing. Current time:" + DateUtil.now());
    ThreadUtil.sleep(1000);
});

// 使用allOf()方法合并T1和T2的CompletableFuture,等待它们都完成
CompletableFuture<Void> bothCompleted = CompletableFuture.allOf(futureT1, futureT2);
// 当T1和T2都完成后,执行T3
bothCompleted.thenRunAsync(() -> System.out.println("T3 is executing after T1 and T2 have completed.Current time:" + DateUtil.now()));
// 等待所有任务完成,验证效果
ThreadUtil.sleep(3000);

同样非常简单,可以通过 CompletableFutureallOf()这个静态方法来并行运行多个 CompletableFuture 。然后,再利用 thenRunAsync()方法即可。

这个问题还有非常多的扩展,上面提到的场景都是异步任务编排最简单的场景。

  推荐阅读:
  1. 我一直在用 Java,但是我一直都不喜欢 Java!
  2. 这些小 Bug,99% 的程序员都写过!
  3. 世界上最难的 5 种编程语言!网友看后惊讶道:竟不是C/C++?
  4. 我年薪40万,让你看看一年能到手多少?
  5. 2024 年 10 月编程语言排行榜|Rust 稳步攀升,即将进入 TIOBE 指数前十!

脚本之家
脚本之家(jb51.net)每天提供最新IT类资讯、原创内容、编程开发的教程与经验分享,送书福利天天在等你!
 最新文章