头条基础架构 - 面试题 - 三个线程并发打出ABC的字母

题目描述

字节跳动基础架构岗的终面问题:
有三个线程,分别能够打出A,B,C三个字母,如何让它们并发协作的打出ABCABCABC…这样的字符串?(仅使用ReentrantLock的功能)

思路一 Lock

使用一个lock与对应创建的condition,思路如下:

  1. 使用一把锁来控制同时三个线程的串行执行,但此时仍无法保证三个线程抢到锁的顺序;
  2. 发现当前不应该自己输出时,使用condition进入await状态;
  3. 在成功打印后,使用condition唤醒其它沉默的线程;

那么通俗来讲,流程就变成三个人不断抢锁,抢到锁后发现没轮到自己干活的时候,就先去睡觉。成功抢到锁并打印内容后,把其它线程叫醒并继续抢锁。直到所有人都输出了指定数量的字母。

public class AbcThread extends Thread {

    private final int index;
    private final Lock lock;
    private final Condition cond;

    private static volatile int cnt = 0;
    private static final char[] CHARS = {'A','B','C'};
    private static final int SIZE = 100;

    public AbcThread(int index, Lock lock, Condition cond) {
        this.index = index;
        this.lock = lock;
        this.cond = cond;
    }

    @Override
    public void run() {
        for (int i = 0; i < SIZE; i++) {
            lock.lock();
            try {
                if (AbcThread.cnt % 3 != index) {
                    cond.await();
                }
                System.out.print(CHARS[index]);
                System.out.flush();
                AbcThread.cnt++;
                cond.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String args[]) {
        Lock lock = new ReentrantLock(true);
        Condition condition = lock.newCondition();
        for (int i=0; i<3; i++) {
            new AbcThread(i, lock, condition).start();
        }
    }
}

改进思路:
创建三个condition,每一个condition只唤醒下一个condition,其余功能保持不变。这样似乎可以减少抢锁后再反复沉睡的损耗——但如果我们使用的是公平锁,则优化后的区别并不大。
原因是公平锁本身就会在抢不到锁时进入排队等待,而我们是保持顺序执行各线程的,也就意味排队也有很大几率是按顺序的。

对于这类无法优化性能的题目,通常都暗含着多个线程的执行本身就不是并发的,它们通常只能进行一种操作,并争抢同一把锁。(因此我们需要一个lock)另一方面争取同一把锁的顺序可能不合预期,需要把不合预期的线程暂时block住,并在必要时需要唤醒。(因此我们需要一个condition)

思路二 信号量

思路一之所以设计复杂,主要是因为三个线程抢一把锁时,顺序难以控制。如果我们使用三把“锁”,每一次只打开一把锁做任务,完成后关闭当前锁(堵塞当前进程)并打开下一把锁(释放下一个线程),就可以圆满的完成任务了。
那为什么不用三个ReentrantLock来实现呢?因为持有lock的线程,想要堵塞住自己又在后续被别人唤醒,就又回到用condition的设计了,并且还用了三个锁!(思路一中仅用单Lock、单Condition也能实现)

Semaphore(信号量)可以设置初始的锁的个数,每个线程可以随意的从信号量中取放任意数量的锁。当信号量中不存在锁时取锁会堵塞,并等到有线程把锁放进来时再拉起。
因为本题可以使用三个信号量分发给三个线程,初始时只有A线程的信号量有锁,因此A线程取走锁并打印后,激活了B线程的信号量;随后B线程也做相同的事,并激活C线程的信号量……

public class SemaphoreThread extends Thread {

    private static int SIZE = 1000;
    private static final char[] CHARS = {'A','B','C'};

    private final int index;
    private final Semaphore current;
    private final Semaphore next;

    public SemaphoreThread(int index, Semaphore current, Semaphore next) {
        this.index = index;
        this.current = current;
        this.next = next;
    }

    public static void main(String[] args) {
        Semaphore[] semaphores = {
            new Semaphore(1), new Semaphore(0), new Semaphore(0)
        };
        for (int i=0; i<3; i++) {
            new SemaphoreThread(i, semaphores[i], semaphores[(i + 1) % 3]).start();
        }
    }

    @Override
    public void run() {
        for (int i = 0; i < SIZE; i++) {
            try {
                current.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print(CHARS[index]);
            next.release();
        }
    }
}

你可能感兴趣的:(并发)