面試題:線程A打印1-10數字,打印到第5個數字時,通知線程B


此題考查的是線程間的通信方式。

  • 可以利用park/unpark實現
  • 可以利用volatile關鍵字實現
  • 可以利用synchronized結合wait notify實現
  • 可以利用JUC中的CountDownLatch實現
  • 可以利用Condition中的await signal 實現

代碼示例

利用Park/Unpak實現線程通信

private void notifyThreadWithParkUnpark(){

        Thread thb  = new Thread("線程B"){
            @Override
            public void run() {
                LockSupport.park();
                System.out.println(Thread.currentThread().getName()+"啟動了");
            }
        };
        Thread tha =new Thread("線程A"){
            @Override
            public void run() {
                for(int i=1;i<11;i++){
                    System.out.println(Thread.currentThread().getName()+i);
                    if(i==5){
                        LockSupport.unpark(thb);
                    }
                }
            }
        };
        thb.start();
        tha.start();
    }

park與unpark可以看做一個令牌,park就是等待令牌,unpark就是頒發一個令牌,另外需要注意的是park與unpark的調用次數不用一一對應,而且假如在同步代碼塊中調用park方法,線程會進入阻塞狀態,但是不會釋放已經占用的鎖。

本例使用park使線程B進入阻塞等待狀態,在線程A調用unpark並傳入線程B的名稱使線程B可以繼續運行。

使用Volatile關鍵字實現線程通信

private static volatile boolean flag = false;

private void notifyThreadWithVolatile(){
        Thread thc= new Thread("線程C"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    if(i==5){
                        flag=true;
                        try {
                            Thread.sleep(500L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(Thread.currentThread().getName()+i);
                }
            }
        };

        Thread thd= new Thread("線程D"){
            @Override
            public void run() {
                while (true){
                    // 防止偽喚醒 所以使用了while
                    while(flag){
                        System.out.println(Thread.currentThread().getName()+"收到通知");
                        break;
                    }
                }
            }
        };

        thd.start();
        try {
            Thread.sleep(1000L);
        } catch (Exception e) {
            e.printStackTrace();
        }
        thc.start();

    }

volatile表示的禁用CPU緩存,用volatile修飾的變量,會強制從主內存中讀取變量的值。java內存模型中關於volatile也是有說明的,volatile只能保證可見性,但不能保證原子性。

本例通過在volatile來修飾一個標志位,線程C修改了該標志位,然后線程D就可以“看到”標志位的修改,從而實現互相通信。

使用Synchronized 集合wait notify實現線程間通信

private static final Object lock = new Object();

private void notifyThreadWithSynchronized(){
        Thread the = new Thread("線程E"){
            @Override
            public void run() {
                synchronized (lock){
                    for (int i = 0; i <10 ; i++) {
                        System.out.println(Thread.currentThread().getName()+i);
                        if(i==5){
                            lock.notify();
                        }
                    }
                }
            }
        };


        Thread thf = new Thread("線程F"){
            @Override
            public void run() {
                while(true){
                    synchronized (lock){
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"啟動了");
                    }
                }
            }
        };
        thf.start();
        try {
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        the.start();

    }

synchronized修飾同步代碼塊,而wait notify notify必須是在synchronized修飾代碼塊中使用,否則會拋出監視器異常。

本實例定義一個對象鎖,而線程F首先獲取到互斥鎖,在執行wait()方法時,釋放已經持有的互斥鎖,進入等待隊列。而線程E執行獲取到互斥鎖開始執行,當1==5時,調用notify方法,就會通知lock的等待隊列,然后線程E會繼續執行,由於線程F此時還是獲取不到互斥鎖(因為被線程E占用),所以會在線程E執行完畢后,才能獲取到執行權。

利用CountDonwLatch實現線程間通信

//      倒計時器
    private CountDownLatch cdl = new CountDownLatch(1);

private void notifyThreadWithCountDownLatch(){
        Thread thg = new Thread("線程G"){
            @Override
            public void run() {
                try {
                    cdl.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"啟動了");
            }
        };

        thg.start();

        Thread thh = new Thread("線程H"){
            @Override
            public void run() {
                for (int i = 1; i < 11; i++) {
                    System.out.println(Thread.currentThread().getName()+i);
                    if(i==5){
                        cdl.countDown();
                    }
                }

            }
        };

        thh.start();
    }

本示例中使用了CountDownLatch倒計時器,利用了倒計時器的阻塞特性來實現等待。具體就是聲明一個計數器為1的倒計時器,線程G調用await()方法進入等待,直到計數器為0的時候才能夠進入執行,而線程H在i==5會將計數器減一,使其為0,此時線程G就會繼續執行了。

利用Condition中的await和signal來實現

//      ReentrantLock+ condition
    private Lock rtl=new ReentrantLock();
    private Condition condition = rtl.newCondition();

private void notifyThreadWithCondition(){

        Thread thi = new Thread("線程I"){
            @Override
            public void run() {

                while (true){
                    rtl.lock();
                    try {
                        condition.await();
                        System.out.println(Thread.currentThread().getName()+"啟動了");
                        break;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        rtl.unlock();
                    }
                }
            }
        };


        Thread thj = new Thread("線程J"){
            @Override
            public void run() {
                rtl.lock();
                try {
                    for (int i = 0; i < 10; i++) {
                        System.out.println(Thread.currentThread().getName()+i);
                        if(i==5){
                            condition.signal();
                        }
                    }
                } finally {
                    rtl.unlock();
                }

            }
        };

        thi.start();
        try {
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thj.start();
    }

本示例是結合ReentrantLock和Condition來進行控制線程間的執行順序,Condition的await()和signal(),他們的語義和wait notify是一樣的。區別是在synchronized代碼塊里調用wait notify。通過示例可以看到這中方法實現會不斷的加鎖與解鎖,所以看起來稍微復雜些。

總結

通過以上代碼看到通過volatile的方式是最簡潔方便,用park與unpark方式是比較靈活,不用加鎖或解鎖,剩下的synchronized與Conditon都是用了鎖,而CountDownLatch則是利用了計數器。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM