分享一道 阿里 筆試題

朋友們許久不見,你們還好嗎?

這段時間裏,我也悄咪咪的去試了試外面的機會,2 年沒有參加面試發現各大廠的面試風格已經悄悄的發生了變化。

前倆年都是喜歡上來一套 JUC 三連炮問到你懵圈爲止,要不就是一套 Mysql 事務三連炮問到你瑟瑟發抖。而現在呢,面試官們喜歡揪着你的項目刨根問底。你可能會說,唉我的項目就是一堆 CRUD,沒啥可說的。

不用擔心,面試官會就你的業務場景,改造一下,現場創造一些難點來讓你提出解決方案。你以爲想到解決方案就完了嗎?too naive!這時候你纔剛剛走進面試官的陷阱,接着面試官會開始對你進行慘無人道(都是搬磚的,相煎何太急)的狂轟亂炸。

此時沒有準備充分的你,就像一隻站在大灰狼面前的小肥羊,孤獨無助,任由大灰狼的皮鞭肆虐。

所以朋友們面試之前一定要多多準備,找幾家不想去的公司試試水,同時也要多刷刷數據結構,線程方面的算法題。

這不,我在阿里第一輪筆試的結束的時候就碰到了這麼一道題:

使用 3 個線程 A,B,C 來按順序依次打印 0 - 100 的數字。

效果爲: A => 1,B => 2,C => 3,A => 4,B => 5,C => 6,A => 7,…..

相信 synchronized 使用熟練的同學可以很快的輸出如下一個解決方案:

public class ThreadPrint {
    private static int number = 1;

    private static final Object LOCK = new Object();

    public static void main(String[] args) {
        Thread a = new Thread(new Runnable(){

            @Override
            public void run() {
                while(true) {
                    synchronized(LOCK) {
                        if (number > 100) {
                            System.out.println("A over");
                            LOCK.notifyAll();
                            break;
                        }
                        if (number % 3 == 1) {
                            System.out.println(Thread.currentThread().getName() + " => " + number);
                            number++;
                        } else {
                            try {
                                LOCK.notifyAll();
                                LOCK.wait();
                            } catch (Exception ex) {
                                System.out.println(ex);
                            }

                        }
                    }
                }
            }
        }, "Thread-A");
        a.start();

        Thread b = new Thread(new Runnable(){

            @Override
            public void run() {
                while(true) {
                    synchronized(LOCK) {
                        if (number > 100) {
                            System.out.println("B over");
                            LOCK.notifyAll();
                            break;
                        }
                        if (number % 3 == 2) {
                            System.out.println(Thread.currentThread().getName() + " => " + number);
                            number++;
                        } else {
                            try {
                                LOCK.notifyAll();
                                LOCK.wait();
                            } catch (Exception ex) {
                                System.out.println(ex);
                            }

                        }
                    }
                }
            }
        }, "Thread-B");
        b.start();

        Thread c = new Thread(new Runnable(){

            @Override
            public void run() {
                while(true) {
                    synchronized(LOCK) {
                        if (number > 100) {
                            System.out.println("C over");
                            LOCK.notifyAll();
                            break;
                        }
                        if (number % 3 == 0) {
                            System.out.println(Thread.currentThread().getName() + " => " + number);
                            number++;
                        } else {
                            try {
                                LOCK.notifyAll();
                                LOCK.wait();
                            } catch (Exception ex) {
                                System.out.println(ex);
                            }

                        }
                    }
                }
            }
        }, "Thread-C");
        c.start();
    }
}

當你樂呵呵的告訴面試官寫完了的時候,他會微微點頭告訴你,嗯,首先這是一個可以 work 的方案,但是呢代碼稍微有些冗餘,可以優化一下嗎。

這三個線程的內部運轉邏輯基本一致,唯一區別就是取模運算時候的值不同,因此我們可以將代碼改進一下:

public class ThreadPrint {
    private static final Object LOCK = new Object();
    public static void main(String[] args) {
       new PrintThread(1, "Thread-A", LOCK).start();
       new PrintThread(2, "Thread-B", LOCK).start();
       new PrintThread(0, "Thread-C", LOCK).start();
    }
}

class PrintThread extends Thread {
    private int mod;
    private String name;
    private static int number = 1;
    private Object LOCK;

    public PrintThread(int mod, String name, Object lock) {
        this.mod = mod;
        this.name = name;
        this.LOCK = lock;
    }

    @Override
    public void run() {
        while(true) {
            synchronized(LOCK) {
                if (number > 100) {
                    System.out.println(name + "over");
                    LOCK.notifyAll();
                    break;
                }
                if (number % 3 == mod) {
                    System.out.println(name + " => " + number);
                    number++;
                } else {
                    try {
                        LOCK.notifyAll();
                        LOCK.wait();
                    } catch (Exception ex) {
                        System.out.println(ex);
                    }

                }
            }
        }
    }
}

當你坑此坑次的精簡了代碼後,面試官笑嘻嘻的說,嗯,現在精簡了不少。突然他臉上漏出了狡黠的笑容,還有其他解法嗎。

嗯,我想想,你陷入了沉思。突然靈光一閃,我可以用信號量來替換管程,接着你寫出瞭如下的代碼:

import java.util.concurrent.Semaphore;

public class ThreadPrint {
    private static final Object LOCK = new Object();
    private static final Semaphore semaphore = new Semaphore(1, false);
    public static void main(String[] args) {
       new PrintThread(1, "Thread-A", semaphore).start();
       new PrintThread(2, "Thread-B", semaphore).start();
       new PrintThread(0, "Thread-C", semaphore).start();
    }
}

class PrintThread extends Thread {
    private int mod;
    private String name;
    private static int number = 1;
    private Semaphore semaphore;

    public PrintThread(int mod, String name, Semaphore semaphore) {
        this.mod = mod;
        this.name = name;
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        while(true) {
            try {
                semaphore.acquire();
                if (number > 100) {
                    System.out.println(name + "over");
                    semaphore.release();
                    break;
                }
                if (number % 3 == mod) {
                    System.out.println(name + " => " + number);
                    number++;
                }
                semaphore.release();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

沒錯,使用管程可以解決的線程間互斥問題,採用信號量一樣可以解決。當你認爲終於可以告一段落的時候,面試官仍然不會放過你,還有其他方法嗎?

你假裝思索片刻,奧奧,還有一種辦法可以使用 ReentrantLock 來解決:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadPrint {
    private static final Lock lock = new ReentrantLock(false);
    public static void main(String[] args) {
       new PrintThread(1, "Thread-A", lock).start();
       new PrintThread(2, "Thread-B", lock).start();
       new PrintThread(0, "Thread-C", lock).start();
    }
}

class PrintThread extends Thread {
    private int mod;
    private String name;
    private static int number = 1;
    private Lock lock;

    public PrintThread(int mod, String name, Lock lock) {
        this.mod = mod;
        this.name = name;
        this.lock = lock;
    }

    @Override
    public void run() {
        while(true) {
            try {
                lock.lock();
                if (number > 100) {
                    System.out.println(name + "over");
                    break;
                }
                if (number % 3 == mod) {
                    System.out.println(name + " => " + number);
                    number++;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }
    }
}

你飛速的敲擊鍵盤,將原本採用 Semaphore 的方案替換爲了 ReentrantLock,心裏想,這下總該滿意了吧。

然而並不像你想象的那麼簡單,面試官再次漏出狡黠的笑容,你現在的方案呢存在一個問題,就是三個線程會同時爭搶資源,這樣會導致某個線程搶佔到了鎖資源,但是又沒到它打印的數字,有沒有辦法優化一下這個問題呢?

你陷入了沉思,不同時競爭鎖,打印又是有序的,那就由 A 線程打印完數字後,A 去喚醒 B,然後 A進入睡眠;接着 B 打印完數字喚醒 C,然後 B 進入睡眠;最後 C 打印數字,然後喚醒 A,接着 C 進入睡眠,以此類推即可完成循環打印,並且同時只有一個線程在運行。

那使用什麼方式來實現呢,聰明的你一定想到了 Condition:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadPrint {
    private static final Lock lock = new ReentrantLock(false);
    private static Condition cOver = lock.newCondition();
    private static Condition aOver = lock.newCondition();
    private static Condition bOver = lock.newCondition();
    public static void main(String[] args) {
       new PrintThread(1, "Thread-A", lock, cOver, bOver).start();
       new PrintThread(2, "Thread-B", lock, aOver, cOver).start();
       new PrintThread(0, "Thread-C", lock, bOver, aOver).start();
    }
}

class PrintThread extends Thread {
    private int mod;
    private String name;
    private static int number = 1;
    private Lock lock;
    private Condition wait;
    private Condition notify;

    public PrintThread(int mod, String name, Lock lock, Condition wait, Condition notify) {
        this.mod = mod;
        this.name = name;
        this.lock = lock;
        this.notify = notify;
        this.wait = wait;
    }

    @Override
    public void run() {
        while(true) {
            try {
                lock.lock();
                if (number > 100) {
                    System.out.println(name + "over");
                    notify.signal();;
                    break;
                }
                if (number % 3 == mod) {
                    System.out.println(name + " => " + number);
                    number++;  
                }
                notify.signal();
                wait.await();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }
    }
}

相信當你作出如上的解決方案後,面試官應該會漏出慈母般的微笑,並且伴隨着頻頻點頭,心裏已經對你紮實的基本功豎起來大拇指。

如上即爲我們今天要介紹的這道併發編程面試題,裏面總共採用了四種方法去解決這個問題,層層遞進不斷優化。

如果你對上面的幾種解法還不能爛熟於心,那你要抓緊鞏固哦,不然容易給面試官留下一個理論王者的面評就不好啦。

 

金三銀四啦,每天一道題目,讓 offer 來得簡單點。

感謝你的閱讀,我爲你準備了一份《高級 Java 面試指南》,點擊在看,關注公衆號,回覆 "禮物" 獲取。
 

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