高併發編程從入門到精通(三)

面試中最常被虐的地方必定有併發編程這塊知識點,不管你是剛剛入門的大四萌新仍是2-3年經驗的CRUD怪,也就是說這類問題你最起碼會被問3年,何不花時間死磕到底。消除恐懼最好的辦法就是面對他,奧利給!(這一系列是本人學習過程當中的筆記和總結,並提供調試代碼供你們玩耍java

上章回顧

1.java線程生命週期和java線程狀態都有哪些?git

2.java線程生命週期之間是如何轉換的?github

3.Thread.start()都作了哪些事情?面試

請自行回顧以上問題,若是還有疑問的自行回顧上一章哦~編程

本章提要

本章學習完成,你將會掌握Thread經常使用API接口的使用,包括sleep、yield和join,而且會詳細解析join源碼和用法。同時配合上一章的start()方法,本章還會介紹一下應該如何去關閉一個線程。鑑於interrupt字段內容較多,咱們放到下一章講哦。(老規矩,熟悉這塊的同窗能夠選擇直接關注點贊👍完成本章學習哦!)bash

本章代碼下載併發

1、Thread經常使用API詳解

本節開頭先打個預防針,針對每個API會用和精通是兩個水準哦,這裏咱們的目標是徹底吃透,因此章節內容會比較幹,可是我會加油寫的有代入感,你們一塊兒加油~👏app


(1) sleep

sleep一共有兩個重載方法ide

  • public static native void sleep(long millis) throws InterruptedException
  • public static void sleep(long millis, int nanos) throws InterruptedException

因爲這兩個實現精度不一樣,內部調用的都是同一個方法,因此咱們這裏就挑public static void sleep(long millis, int nanos) throws InterruptedException來看下函數

/**
     * Causes the currently executing thread to sleep (temporarily cease
     * execution) for the specified number of milliseconds plus the specified
     * number of nanoseconds, subject to the precision and accuracy of system
     * timers and schedulers. The thread does not lose ownership of any
     * monitors.
     *
     * @param  millis
     *         the length of time to sleep in milliseconds
     *
     * @param  nanos
     *         {@code 0-999999} additional nanoseconds to sleep
     *
     * @throws  IllegalArgumentException
     *          if the value of {@code millis} is negative, or the value of
     *          {@code nanos} is not in the range {@code 0-999999}
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        sleep(millis);
    }
複製代碼

官方的描述是這樣的,使線程暫時中止執行,在指定的毫秒數上再加上指定的納秒數,可是線程不會失去監視器。這裏的關鍵是不會失去持有的監視器,上一章咱們講過這時線程處於BLOCKED階段。

if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
複製代碼

當millis<0或者nanos不在0-999999範圍中的時候就會拋出IllegalArgumentException

@throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
複製代碼

sleep可被中斷方法打斷,可是會拋出InterruptedException異常。

好啦到這裏咱們介紹完了這個API了,是否是感受很簡單呢?哈哈光這樣可不行,實踐是檢驗真理的惟一標準下面咱們來驗證一下sleep以後對象監視鎖到底有沒有釋放。

別犯困啦,劃重點啦

/**
   * 建立一個獨佔鎖
   */
  private static final Lock lock = new ReentrantLock();

  public static void main(String[] args) {

    new Thread(new Runnable() {
      @Override
      public void run() {
        lock.lock();
        System.out.println("我是" + Thread.currentThread().getName() + ",lock在我手中");
        try {
          Thread.sleep(3000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          lock.unlock();
          System.out.println(Thread.currentThread().getName() + "不須要lock了");
        }

      }
    }, "一號線程").start();

    new Thread(new Runnable() {
      @Override
      public void run() {
        lock.lock();
        System.out.println("我是" + Thread.currentThread().getName() + ",lock在我手中");
        try {
          Thread.sleep(3000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          lock.unlock();
          System.out.println(Thread.currentThread().getName() + "不須要lock了");
        }

      }
    }, "二號線程").start();

  }
複製代碼

輸出結果:

我是一號線程,lock在我手中
一號線程不須要lock了
我是二號線程,lock在我手中
二號線程不須要lock了
複製代碼

同窗們能夠用各類姿式來run咱們的代碼,不關你是坐着run,躺着run仍是倒立run,結果始終是連續的,不會出現一號線程和二號線程交替打印的情景。這就證實了sleep確實不會釋放其獲取的監視鎖,可是他會放棄CPU執行權。實踐也實踐完了,可是每次都要計算毫秒也着實費勁,有沒有什麼好的辦法呢?


⚠️會玩的都這麼寫

假如如今有需求要求咱們讓線程sleep1小時28分19秒33毫秒咱們要咋辦?手腳快的同窗可能已經掏出了祖傳的計算器滴滴滴地操做起來了。可是咱們通常不這麼作,JDK1.5爲咱們新增了一個TimeUnit枚舉類,請你們收起心愛的計算器,其實咱們能夠這麼寫

//使用TimeUnit枚舉類
          TimeUnit.HOURS.sleep(1);
          TimeUnit.MINUTES.sleep(28);
          TimeUnit.SECONDS.sleep(19);
          TimeUnit.MILLISECONDS.sleep(33);
複製代碼

這樣咱們的代碼更加優雅,可讀性會更強

寫累了,鍛鍊下身體,給同窗們挖個坑。咱們已經知道millis的範圍是大於等於0,sleep(1000)咱們知道是什麼意思,那麼sleep(0)會有做用嗎?

答案是會起做用的,這是咱們須要記住的關於 sleep的第二個點, sleep(0)的做用是「觸發操做系統馬上從新進行一次CPU競爭,競爭結果多是當前線程繼續獲取到CPU的執行權,也有多是別的線程獲取到了當前線程的執行權。

兩個點但願你們能夠牢記

1.sleep不會釋放mointor lock。

2.sleep的做用是觸發操做系統馬上從新進行一次CPU競爭。

(2) yield

仍是老套路,咱們先來看API接口描述是怎麼定義這個接口的

/**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * <p> Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * <p> It is rarely appropriate to use this method. It may be useful
     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
     */
    public static native void yield();
複製代碼

這個方法的描述是這樣的提示調度程序,當前線程願意放棄CPU執行權。調度程序能夠無條件忽略這個提示,打個比方就是說,A暗戀B,A說我願意怎麼怎麼樣,B能夠接受A,可是也能夠徹底無條件的忽略A,,嗯嗯額~大概就是這麼個場景,卑微A。

API接口描述中也明確說明了,這個接口不經常使用,可能用於調試或測試的目的,可能用於重現因爲競爭條件而致使的bug,還有就是在java.util.concurrent.locks包中有用到這個API,總的來講就是在實際生產開發過程當中是不用的。可是它又不像stop同樣已經被廢棄不推薦使用,講這個API的目的是應由於它很容易和sleep混淆。

1.調用yield並生效以後線程會從RUNNING階段轉變爲RUNNABLE,固然被無條件忽略的狀況除外。而sleep則是進入BLOCKED階段,並且是幾乎百分百會進入。

2.sleep會致使線程暫停,可是不會消耗CPU時間片,yield一旦生效就會發生線程上下文切換,會帶來必定的開銷。

3.sleep能夠被另外一個線程調用interrupt中斷,而yield就不會,yield得等到CPU輪詢給到執行權的時候纔會再次被喚醒,也就是從RUNNABLE階段編程RUNNING階段。

光說不練假把式,雖然不經常使用,可是是驢子是馬總歸仍是要溜一溜。

private static class MyYield implements Runnable {

    @Override
    public void run() {
      for (int i = 0; i < 5; i++) {
        if (i % 5 == 0) {
          System.out.println(Thread.currentThread().getName()+"線程,yield 它出現了");
//          Thread.yield();

        }
      }

      System.out.println(Thread.currentThread().getName()+"結束了");
    }
  }

  public static void main(String[] args) {
    Thread t1 = new Thread(new MyYield());
    t1.start();

    Thread t2 = new Thread(new MyYield());
    t2.start();

    Thread t3 = new Thread(new MyYield());
    t3.start();
  }
複製代碼

屢次執行,按到最多的輸出是連續的,相似下面這種輸出結果:

Thread-0線程,yield 它出現了
Thread-0結束了
Thread-1線程,yield 它出現了
Thread-1結束了
Thread-2線程,yield 它出現了
Thread-2結束了
複製代碼

如今咱們把註釋打開,發現輸出結果變了

Thread-0線程,yield 它出現了
Thread-1線程,yield 它出現了
Thread-2線程,yield 它出現了
Thread-0結束了
Thread-1結束了
Thread-2結束了
複製代碼

那是應爲在調用到yield到時候當前線程讓出了執行權,因此等到你們都出現了以後,你們再分別結束了

(3) join

在本小節中咱們介紹一下joinAPI

  • public final void join() throws InterruptedException
  • public final synchronized void join(long millis)
  • public final synchronized void join(long millis, int nanos) 和sleepAPI十分相像,可是join除了兩個設置超時等待時間的API外,還額外提供了一個不設置超時時間的方法,可是經過追蹤第一個API咱們發現內部其實調用的就是第二個API的join(0),設置納秒的內部調用也是第二個API。因此咱們這邊就拿第二個API來說解。
/**
    //設置一段時間等待當前線程結束,若是超時還未返回就會一直等待
     * Waits at most {@code millis} milliseconds for this thread to
     * die. A timeout of {@code 0} means to wait forever.
     *
     這個方法調用的前提就是當前線程仍是處於alive狀態的
     * <p> This implementation uses a loop of {@code this.wait} calls
     * conditioned on {@code this.isAlive}. As a thread terminates the
     * {@code this.notifyAll} method is invoked. It is recommended that
     * applications not use {@code wait}, {@code notify}, or
     * {@code notifyAll} on {@code Thread} instances.
     *
     * @param  millis
     *         the time to wait in milliseconds
     *
     //超時時間爲負數
     * @throws  IllegalArgumentException
     *          if the value of {@code millis} is negative
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
複製代碼

經過該方法咱們能夠看到他的邏輯是經過當前運行機器的時間,判斷線程是否isAlive來決定是否須要繼續等待,而且內部咱們能夠看到調用的是wait()方法,直到delay<=0的時刻,就會跳出當前循環,從而結束中斷。

又要給同窗們講一個悲傷的故事了

public static void main(String[] args) throws InterruptedException {

    Thread t1 = new Thread(() -> {
      System.out.println("週末都要加班,終於回家了,洗個手吃飯了");
      try {
        TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.print("洗完手,");
    });

    Thread t2 = new Thread(() -> {
      try {
        TimeUnit.SECONDS.sleep(2);
//        t1.join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.print("拿起筷子,");
    });

    t1.start();
    t2.start();
//    t2.join();
    System.out.println("我要吃飯了");

  }
複製代碼

輸出:

週末都要加班,終於回家了,洗個手吃飯了
我要吃飯了
拿起筷子,洗完手,
複製代碼

顯然這個結果不是咱們想要的結果,也可是不排除加班加的已經意識模糊,手抓飯了,這裏咱們仍是但願按照正常習慣來執行。咱們把註釋打開

輸出:

週末都要加班,終於回家了,洗個手吃飯了
洗完手,拿起筷子,我要吃飯了
複製代碼

這個纔是咱們須要的結果。

相信同窗們經過這個例子已經大概瞭解join的做用了,沒錯join是可讓程序能按照必定的次序來完成咱們想完成的工做,他的工做原理就是阻塞當前調用join的線程,讓新join進來的線程優先執行。

2、線程該如何關閉

線程關閉大體上能夠分爲三種狀況

1.線程正常關閉

2.線程異常退出

3.進程假死

這裏咱們着重講一下線程正常關閉的狀況,也是實際開發生產中經常使用方法。

1.線程生命週期正常結束

這個沒什麼好說的,就是線程邏輯單元執行完成而後本身正常結束。

2.捕獲中斷信號關閉線程。

早期JDK中還提供有一個stop函數用於關閉銷燬線程,可是後來發現會存在monitor鎖沒法釋放的問題,會致使死鎖,因此如今強烈建議你們不要用這個方式。這裏咱們使用捕獲線程中斷的方式來結束線程。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      System.out.println("我要自測代碼啦~~");
      while (!Thread.currentThread().isInterrupted()) {
        System.out.println("目前來看是好好的");
      }
      System.out.println("代碼中斷中止了");
    });

    t1.start();
    TimeUnit.SECONDS.sleep(1);
    t1.interrupt();

  }
複製代碼

輸出:

我要自測代碼啦~~
...
目前來看是好好的
目前來看是好好的
代碼中斷中止了
複製代碼

能夠看到,咱們經過判斷當前線程的isInterrupted()狀態來捕獲線程是否已經被中斷,從而能夠來控制線程正常關閉。同理,若是咱們在線程內部已經執行來某中斷方法,好比sleep就能夠經過捕獲中斷異常來退出sleep狀態,從而也能讓線程正常結束。

3.設置開關關閉

因爲interrupt頗有可能被擦除,或者整個邏輯單元中並有調用中斷方法,這樣咱們上一種方法就不適用了,這裏咱們使用volatile關鍵字來設置一個開關,控制線程的正常退出。

private static class MyInterrupted extends Thread {

    private volatile boolean close = false;

    @Override
    public void run() {
      System.out.println("我要開始自測代碼啦~~");
      while (!close) {
        System.out.println("目前來看好好的");
      }
      System.out.println("close已經變成了" + close + ",代碼正常關閉了");
    }

    public void closed() {
      this.close = true;
    }
  }

  public static void main(String[] args) throws InterruptedException {
    MyInterrupted myInterrupted = new MyInterrupted();
    myInterrupted.start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("我要開始關閉線程了");
    myInterrupted.closed();
  }
複製代碼

輸出:

我要開始自測代碼啦~~
...
目前來看好好的
目前來看好好的
目前來看好好的
我要開始關閉線程了
close已經變成了true,代碼正常關閉了
複製代碼

能夠看到咱們調用closed方法時候把close設置爲了true,從而正常關閉代碼,關於volatile關鍵字咱們以後的章節會詳細講哦,請同窗們繼續關注,和我一塊兒學習😁,但願能夠同窗們幫忙關注下和點點贊👍哦~

下一章也已經出來咯,此次內容稍多,因此拖了比較久,可是內容都是妥妥的,下一章詳細講解了interrupt的執行邏輯,一步一步帶同窗們調試,感興趣的同窗記得進入下一章的學習哦~

相關文章
相關標籤/搜索