頭條基礎架構 - 面試題 - 三個線程併發打出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();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章