白話解析平安筆試題:多線程交替打印

題目:請使用A,B 2個線程,交替打印1-100,提供2種以上的實現方式 ?

說實話,我很懶,沒刷過什麼多線程相關的題目,和大多數人一樣只是知道一些多線程的基礎知識,所以第一眼看到這個題目是有點懵的。寫這篇文章主要是分享一下我的思考過程,如何通過自己已知的信息,將其整合起來去解決這道問題,這是一種信息整合能力。

現在我們先來剖析一下題目:A、B 2個線程,交替打印。也就是說在同一時間只能有一個線程在執行打印,這2個線程雖然是執行同樣的功能代碼,但是互斥的。

我們先來看第一種執行方式,直接上代碼:

public class AlternatePrinting1 implements Runnable {
    private AtomicInteger index=null;
    private int flag=0;
    public AlternatePrinting1(AtomicInteger index,int flag){
        this.index=index;
        this.flag=flag;
    }
    @Override
    public void run() {
        while (index.get()<101){
            if(index.get()%2==flag){
                System.out.println((flag==1?"A:":"B:")+index.get());
                index.getAndIncrement();
            }
        }
    }
}
public class TestQ1 {
    public static void main(String[] args) {
        AtomicInteger index=new AtomicInteger(1);
        Thread A=new Thread(new AlternatePrinting1(index,1));
        Thread B=new Thread(new AlternatePrinting1(index,0));
        A.start();
        B.start();
    }
}

思考過程:最開始我想的是直接弄個for循環打印1-100,然後考慮如何交替,後來發現此路不通,主要是對多線程理解不夠。它得交替打印,那變量就得在2個線程間共享,那麼我們只需要將打印和自增這2個操作區分成2種情況,讓AB分別去執行就行了。那麼怎麼區分2種情況?先別急,我們先來想下,什麼是交替:ABABAB....,比如 A線程打印 1,B線程打印2,A線程打印3....像這樣交換執行。所以很容易想到整數分爲奇數和偶數,直接用一個變量對2取餘結果爲0和1 就可以區分這2種情況了。

第二種:

public class TestQ1Improve {
    static volatile int index = 1;
    public static void main(String[] args) {
        Thread A = new Thread(() -> {
            alternatePrinting(1);
        });
        Thread B = new Thread(() -> {
            alternatePrinting(0);
        });
        A.start();
        B.start();
    }
    public static void alternatePrinting(int flag) {
        while (index < 101) {
            if (index % 2 == flag) {
                System.out.println((flag == 1 ? "A:" : "B:") + index);
                index++;
            }
        }
    }
}

思考過程:第一種是我最開始想到的實現方式,這種方式和第一種其實差不多,核心都是通過奇偶數來區分2種情況,不同點是:第一種採用的是線程安全的 AtomicInteger 變量,這種是採用的非線程安全的volatile修飾的整型變量。其實是對第一種的優化。首先在上面第一種執行方式的代碼中可以看到,A、B2個線程雖然是共享的線程安全的index變量,但是在同一時間 打印index 和 index 的自增是隻有一個線程在執行的,只有在while循環判斷時,會去共享的讀取index變量,所以將其改成採用volatile 的修飾的整型變量即可。

我們繼續來看第三種執行方式:

public class TestQ2 {
    private static volatile int i = 1;
    public static void main(String[] args) throws Exception {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    while (i < 101) {
                        System.out.println(Thread.currentThread().getName() + ":" + i++);
                        try {
                            notify();
                            wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    notify();
                }
            }
        };
        Thread A = new Thread(runnable, "A");
        Thread B = new Thread(runnable, "B");
        A.start();
        B.start();
    }
}

思考過程:這種實現方式的需要考慮的是線程間的通信,這裏優先想到的是wait/notify的線程通信方式,所以先用 synchronized將其鎖住,然後將A、B 2個線程交替等待喚醒就可以實現交替打印了

下面來看第四種

public class TestQ2Extend {
    private static volatile int i = 1;
    public static void main(String[] args){
         Lock lock = new ReentrantLock();
         Condition condition = lock.newCondition();
        Runnable runnable = ()-> {
                try {
                    lock.lock();
                    while (i < 101) {
                        System.out.println(Thread.currentThread().getName() + ":" + i++);
                        condition.signal();
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    condition.signal();
                }finally {
                    lock.unlock();
                }
        };
        Thread A = new Thread(runnable, "A");
        Thread B = new Thread(runnable, "B");
        A.start();
        B.start();
    }
}

思考過程:這種方式和第三種的核心思路一樣,可以說是第三種的擴展實現,都是採用的線程通信的思路來寫的,不同點就是這個是採用 Lock 和 Condition的await、signal 方法來實現線程通信,進而達到交替等待喚醒就可以了。

總結:

上面四種實現的線程交替的打印方式可以歸納爲2種策略:

1. 自旋:那麼什麼是自旋:簡單來說就是循環等待。第一種和第二種都是採用自旋的方式,實現線程的交替打印,基於這種策略可以有很多種實現。比如可以採用布爾變量的true和false來區分A、B 2個線程,每自增一次就改變一次布爾變量的值即可。

2. 線程通信:第三種和第四種都是採用線程通信的方式實現,第三種是wait/notify 線程通信,第四種是Condition的await/signal線程通信。如果有其他的線程通信方式也可以直接套用上面的代碼來進行實現。

 

題外話:筆試題的數量很多,單純刷是沒法刷完的,我們需要調整刷題的策略,當我們在遇到每一道筆試題的時候不要急,慢慢來爭取能夠吃透它。怎麼吃透一道面試題呢?首先我們得改變原有的思維方式,當我們面對一道面試題的時候,今天我想講的:不是如何去解決問題,而是如何去思考問題,這很關鍵。有時候我們無法解出一道筆試題,不是我們能力不夠,很可能是思維方式不對,即使解出來了,也僅僅是滿足當前的答案,沒有去思考是否有其他相似的場景可以套用,或者是否有更好的方案去解決問題。當我們受困於一道面試題的時候,不妨考慮跳出當前的思路,去重新審視一下題目再思考。

        說了這麼多,當面對一道筆試題時,應該怎麼做:舉一反三,追求極致,覆盤總結。當面對每個問題的時候,你都能以這種方式思考,隨着時間的推移,你的思維能力會越來越強,學習能力也會穩步上升

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章