1. 程式人生 > >【面試】如果把執行緒當作一個人來對待,所有問題都瞬間明白了

【面試】如果把執行緒當作一個人來對待,所有問題都瞬間明白了

多執行緒的問題都曾經困擾過每個開發人員,今天將從全新視角來解說,希望讀者都能明白。

強烈建議去執行下文章中的示例程式碼,自己體會下。



問題究竟出在哪裡?


一個執行緒執行,固然是安全的,但是有時太慢了,怎麼辦?

老祖宗告訴我們,“一方有難,八方支援”,那不就是多叫幾個執行緒來幫忙嘛,好辦呀,多new幾個不就行了,又不要錢。這樣能管用嗎?繼續往下看。

俗話說,“在家靠父母,出門靠朋友”。有了朋友的幫助,就會事半功倍。是這樣的嗎?

不一定,如果朋友“不靠譜”,結果竟是在“添亂”。於是就演變為,“不怕神一樣的對手,就怕豬一樣的隊友”。可見“人多力量大”縱然是對的,但也要配合好才能成事。


人和人是朋友,那執行緒和執行緒也是“朋友”,如果多執行緒之間不能配合好的話,最終也會變為“豬一樣的隊友”。事實證明,這也不是一件易事。且容我慢慢道來。

開發是一門技術,管理是一門藝術。也許你正想帶著兄弟們大幹一場,可偏偏就有人要辭職。或者你付出了這麼多,但別人從來沒有感動過。為什麼會這樣呢?

因為你面對的是人。每個人都是獨立的個體,有思想,有靈魂,有情感,有三觀。能夠接受外界的“輸入”,經過“處理”後,能夠產生“輸出”。

說白了就是會自主的分析問題,並做出決定。這叫什麼呢?答案就是,主觀能動性。

擁有主觀能動性的物體(比如人),你需要和它協商著或配合著來共同完成一件事情,而不能“強迫”它去做什麼,因為這樣往往不會有好的結果。

費了這麼多口舌,就是希望把問題儘量的簡單化。終於可以回到程式了,那執行緒的情況是不是類似的呢?答案是肯定的。

一個執行緒準備好後,經過CPU的排程,就可以自主的運行了。此時它儼然成了一個獨立的個體,且具有主觀能動性。

這本是一件好事,但卻也有不好的一面,那就是你對它的“掌控”能力變弱了,頗有一種“將在外,君命有所不受”的感覺。

可能你不同意這種看法,說我可以“強迫”它停止執行,呼叫Thread類的stop()方法來直接把它“掐死”,不好意思,該方法已廢棄。

因為執行緒可能在執行一些“關鍵”程式碼(比如轉賬),此刻不能被終止。Thread類還有一些其它的方法也都廢棄了,大抵原因其實都差不多。

講了這麼多,相信你已經明白了,簡單總結一下:

事情起因:執行緒可以獨立自主的執行,可以認為它具有主觀能動性。

造成結果:對它的掌控能力變弱了,而且又不能直接把它“幹掉”。

解決方案:凡事商量著來,互相配合著把事情完成。

作者觀點:其實就是把執行緒當作人來對待。



小試牛刀一下


一旦把執行緒當成人,就來到了人類的世界,這我們太熟悉了,所以很多問題都會變得非常簡單明瞭。一起來看看吧。

場景一,停止

“大胖,大胖,12點了,該去吃飯了,別寫了”

“好的,好的,稍等片刻,把這幾行程式碼寫完就走”

要點:把停止的訊號傳達給別人,別人處理完手頭的事情就自己主動停止了。

 static void stopByFlag() {
    ARunnable ar = new ARunnable();
    new Thread(ar).start();
    ar.tellToStop();
  }

  static class ARunnable implements Runnable {

    volatile boolean stop;

    void tellToStop() {
      stop = true;
    }

    @Override
    public void run() {
      println("進入不可停止區域 1。。。");
      doingLongTime(5);
      println("退出不可停止區域 1。。。");
      println("檢測標誌stop = %s", String.valueOf(stop));
      if (stop) {
        println("停止執行");
        return;
      }
      println("進入不可停止區域 2。。。");
      doingLongTime(5);
      println("退出不可停止區域 2。。。");
    }

  }

 

解說:執行緒在預設的地點檢測flag,來決定是否停止。


場景二,暫停/恢復

“大胖,大胖,先別發請求了,對方伺服器快掛了”

“好的,好的,等這個執行完就不發了”

過了一會

“大胖,大胖,可以重新發請求了”

“好的,好的”

要點:把暫停的訊號傳達給別人,別人處理完手頭的事情就自己主動暫停了。但是恢復是無法自主進行的,只能由作業系統來恢復執行緒的執行。

 

static void pauseByFlag() {
    BRunnable br = new BRunnable();
    new Thread(br).start();
    br.tellToPause();
    sleep(8);
    br.tellToResume();
  }

  static class BRunnable implements Runnable {

    volatile boolean pause;

    void tellToPause() {
      pause = true;
    }

    void tellToResume() {
      synchronized (this) {
        this.notify();
      }
    }

    @Override
    public void run() {
      println("進入不可暫停區域 1。。。");
      doingLongTime(5);
      println("退出不可暫停區域 1。。。");
      println("檢測標誌pause = %s", String.valueOf(pause));
      if (pause) {
        println("暫停執行");
        try {
          synchronized (this) {
            this.wait();
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        println("恢復執行");
      }
      println("進入不可暫停區域 2。。。");
      doingLongTime(5);
      println("退出不可暫停區域 2。。。");
    }

  }

解說:還是在預設的地點檢測flag。然後就是wait/notify配合使用。


場景三,插隊

“大胖,大胖,讓我站到你前面,不想排隊了”

“好吧”

要點:別人插隊到你前面,必須等他完事後才輪到你。


static void jqByJoin() {
    CRunnable cr = new CRunnable();
    Thread t = new Thread(cr);
    t.start();
    sleep(1);
    try {
      t.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    println("終於輪到我了");
  }

  static class CRunnable implements Runnable {

    @Override
    public void run() {
      println("進入不可暫停區域 1。。。");
      doingLongTime(5);
      println("退出不可暫停區域 1。。。");
    }

  }

 

解說:join方法可以讓某個執行緒插到自己前面,等它執行完,自己才會繼續執行。


場景四,叫醒

“大胖,大胖,醒醒,醒醒,看誰來了”

“誰啊,我去”

要點:要把別人從睡夢中叫醒,一定要採取稍微暴力一點的手段。

static void stopByInterrupt() {
    DRunnable dr = new DRunnable();
    Thread t = new Thread(dr);
    t.start();
    sleep(2);
    t.interrupt();
  }

  static class DRunnable implements Runnable {

    @Override
    public void run() {
      println("進入暫停。。。");
      try {
        sleep2(5);
      } catch (InterruptedException e) {
        println("收到中斷異常。。。");
        println("做一些相關處理。。。");
      }
      println("繼續執行或選擇退出。。。");
    }

  }

 

解說:執行緒在sleep或wait時,是處於無法互動的狀態的,此時只能使用interrupt方法中斷它,執行緒會被啟用並收到中斷異常。



常見的協作配合


上面那些場景,其實都是對一個執行緒的操作,下面來看多執行緒間的一些配合。

事件一,考試

假設今天考試,20個學生,1個監考老師。規定學生可以提前交卷,即把卷子留下,直接走人就行了。

但老師必須等到所有的學生都走後,才可以收卷子,然後裝訂打包。

如果把學生和老師都看作執行緒,就是1個執行緒和20個執行緒的配合問題,即等20個執行緒都結束了,這1個執行緒才開始。

比如20個執行緒分別在計算資料,等它們都結束後得到20箇中間結果,最後這1個執行緒再進行後續彙總、處理等。

  static final int COUNT = 20;
  static CountDownLatch cdl = new CountDownLatch(COUNT);

  public static void main(String[] args) throws Exception {
    new Thread(new Teacher(cdl)).start();
    sleep(1);
    for (int i = 0; i < COUNT; i++) {
      new Thread(new Student(i, cdl)).start();
    }
    synchronized (ThreadCo1.class) {
      ThreadCo1.class.wait();
    }
  }

  static class Teacher implements Runnable {

    CountDownLatch cdl;

    Teacher(CountDownLatch cdl) {
      this.cdl = cdl;
    }

    @Override
    public void run() {
      println("老師髮捲子。。。");
      try {
        cdl.await();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      println("老師收卷子。。。");
    }

  }

  static class Student implements Runnable {

    CountDownLatch cdl;
    int num;

    Student(int num, CountDownLatch cdl) {
      this.num = num;
      this.cdl = cdl;
    }

    @Override
    public void run() {
      println("學生(%d)寫卷子。。。", num);
      doingLongTime();
      println("學生(%d)交卷子。。。", num);
      cdl.countDown();
    }

  }

 

解說:每完成一個執行緒,計數器減1,當減到0時,被阻塞的執行緒自動執行。


事件二,旅遊

最近景色宜人,公司組織去登山,大夥都來到了山腳下,登山過程自由進行。

但為了在特定的地點拍集體照,規定1個小時後在半山腰集合,誰最後到的,要給大家表演一個節目。

然後繼續登山,在2個小時後,在山頂集合拍照,還是誰最後到的表演節目。

接著開始下山了,在2個小時後在山腳下集合,點名回家,最後到的照例表演節目。

  static final int COUNT = 5;
  static CyclicBarrier cb = new CyclicBarrier(COUNT, new Singer());

  public static void main(String[] args) throws Exception {
    for (int i = 0; i < COUNT; i++) {
      new Thread(new Staff(i, cb)).start();
    }
    synchronized (ThreadCo2.class) {
      ThreadCo2.class.wait();
    }
  }

  static class Singer implements Runnable {

    @Override
    public void run() {
      println("為大家唱歌。。。");
    }

  }

  static class Staff implements Runnable {

    CyclicBarrier cb;
    int num;

    Staff(int num, CyclicBarrier cb) {
      this.num = num;
      this.cb = cb;
    }

    @Override
    public void run() {
      println("員工(%d)出發。。。", num);
      doingLongTime();
      println("員工(%d)到達地點一。。。", num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)再出發。。。", num);
      doingLongTime();
      println("員工(%d)到達地點二。。。", num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)再出發。。。", num);
      doingLongTime();
      println("員工(%d)到達地點三。。。", num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)結束。。。", num);
    }

  }

 

解說:某個執行緒到達預設點時就在此等待,等所有的執行緒都到達時,大家再一起向下個預設點出發。如此迴圈反覆下去。


事件三,勞動

大胖和小白去了創業公司,公司為了節約開支,沒有請專門的保潔人員。讓員工自己掃地和擦桌。

大胖覺得擦桌輕鬆,就讓小白去掃地。可小白覺得掃地太累,也想擦桌。

為了公平起見,於是決定,每人先幹一半,然後交換工具,再接著幹對方剩下的那一個半。

  static Exchanger<Tool> ex = new Exchanger<>();

  public static void main(String[] args) throws Exception {
    new Thread(new Staff("大胖", new Tool("笤帚", "掃地"), ex)).start();
    new Thread(new Staff("小白", new Tool("抹布", "擦桌"), ex)).start();
    synchronized (ThreadCo3.class) {
      ThreadCo3.class.wait();
    }
  }

  static class Staff implements Runnable {

    String name;
    Tool tool;
    Exchanger<Tool> ex;

    Staff(String name, Tool tool, Exchanger<Tool> ex) {
      this.name = name;
      this.tool = tool;
      this.ex = ex;
    }

    @Override
    public void run() {
      println("%s拿的工具是[%s],他開始[%s]。。。", name, tool.name, tool.work);
      doingLongTime();
      println("%s開始交換工具。。。", name);
      try {
        tool = ex.exchange(tool);
      } catch (Exception e) {
        e.printStackTrace();
      }

      println("%s的工具