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

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

上章回顧

1.併發編程主要解決的問題是什麼?git

2.線程建立和實現的方式有哪些?github

3.new Thread()是如何一穿三的呢?面試

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

本章提要

本章學習完成,你將會對線程的生命週期有清楚的認識,而且明白不一樣狀態之間是如何轉換的,以及對java線程狀態枚舉類解讀。此外本章還會着重對Thread.start()進行細緻的解讀和分析,同時經過案例你能夠更加清晰的明白start()run()之間的關係。(老規矩,熟悉這塊的同窗能夠選擇直接點贊👍完成本章學習哦)設計模式

本章代碼下載數組

1、線程生命週期

相信有很多同窗看到這四個字就氣不打一出來,面試老是被問及這生命週期是啥,那生命週期你來描述一下的,一下脾氣就上來了。確實這個東西是有點煩,可是我的沒什麼技巧,死記硬背是最簡單最好用的方式。等到背熟了,天然而然在用到的時候就會融會貫通,甚至會衍生出本身的獨有的思路。不說太多,概念性的東西背就完事兒了,上菜~bash

1.NEW階段

NEW階段就是你new Thread()建立線程對象時候的階段。併發

劃重點了jvm

NEW階段下其實線程根本仍是不存在的,咱們只是建立了一個Therad對象,就和咱們最經常使用的new關鍵字是一個道理。只有當咱們真正把線程啓動起來的時候,此時纔會在JVM進程中把咱們的線程建立出來。

按照上一章的思路,咱們new了一個Thread對象以後就須要調用Thread.start()來啓動線程,此時線程會從NEW階段轉換到RUNNABLE階段。

2. RUNNABLE階段

只有調用Thread.start()方法才能使線程從NEW階段轉換到RUNNABLE階段。

固然咱們從字面意思也能夠知道此時線程是處於可執行轉狀態而不是真正的執行中狀態了,此時的線程只能等CPU翻牌子,翻到了他才能真正的跑起來。有些同窗可能會說要是CPU一直不翻牌子咋辦?嚴格意義上來說,處在RUNNABLE的線程只有兩條出路,一條是線程意外退出,還有一條是被CPU翻牌子進入RUNNING階段。


到這裏一切看起來仍是那麼簡單,那麼美好,new一個Thread對象,而後調用start啓動起來,而後等翻CPU翻牌子以後進入RUNNING階段。能夠打個比方,NEW階段的時候咱們的線程仍是宮外的一位佳人對象,調用start方法以後就搖身一變成爲宮裏的一位小主了,也就是中間階段RUNNABLE,等到獲取到CPU調度執行權的時候就晉升爲得寵的娘娘了,也就是進入了RUNNING階段。可想而知,此時咱們的線程娘娘必定是比宮外的佳人要複雜的多了。


3.RUNNING階段

⚠️注意

有了解過這塊內容的同窗看到這裏可能會有疑問,java線程狀態中並無這個狀態,爲何咱們在講生命週期的時候會把這一狀態單獨拆分出來作講解?爲了章節內容的流暢性,這塊內容的解釋放到下一節去講解,這邊咱們仍是繼續講咱們的線程生命週期


好的咱們繼續

這個階段的線程已經獲取到了CPU調度執行權,也就是說處於運行中狀態了。

在該階段中,線程能夠向前或者向後發生轉換:

1.因爲CPU的調度器輪詢致使該線程放棄執行,就會進入RUNNABLE階段。

2.線程主動調用yield,放棄CPU執行權,就會進入RUNNABLE階段(這種方式並非百分百生效的,在CPU資源不緊張的時候不會生效)。

3.調用sleepwait方法,進入BLOCKED階段(這裏講的BLOCKED階段和線程的BLOCKED狀態須要區分開,這邊講的是一個比較廣義的BLOCKED的階段

4.進行某個阻塞的IO操做而進入BLOCKED階段

5.爲了獲取某個鎖資源而加入到該鎖到阻塞隊列中而進入BLOCKED階段

6.線程執行完成或者調用stop方法或者判斷某個邏輯標識,直接進入TERMINATED階段

4.BLOCKED階段

進入該階段的緣由已經在RUNNING階段闡述過了,這裏就再也不說明,這裏主要介紹一下處於該階段的線程能夠如何切換。 1.直接進入TERMINATED,好比調用stop方法或者意外死亡(JVM Crash)

2.線程阻塞的操做結束,讀取或者寫入了想操做的數據進入RUNNABLE狀態

3.線程完成了指定時間的休眠,進入RUNNABLE狀態

4.Wait狀態的線程被notify或者notifyall喚醒,進入RUNNABLE狀態

5.獲取到了鎖資源,進入RUNNABLE狀態

6.線程阻塞過程被打斷,好比調用interrupt方法,進入RUNNABLE狀態

5.TERMINATED狀態

TERMINATED狀態是一個線程的最終狀態,在該狀態中線程不會切換到其餘任何狀態,線程進入TERMINATED狀態意味着線程整個生命週期結束了,進入TERMINATED狀態的方式有如下三種:

1.線程正常結束

2.線程意外結束

3.JVM Crash

2、線程生命週期和java線程狀態如何對應起來(源碼分析)

面對這個問題,最直接的論證方式必定就是看代碼,請同窗們先找到java.lang.Thread.State這個枚舉類。

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
複製代碼

能夠看到這邊state枚舉類包含有6中線程狀態,根聽說明咱們一一來解讀這六個狀態。

(1)NEW狀態 = NEW階段

* <li>{@link #NEW}<br>
     *     A thread that has not yet started is in this state.
     *     </li>
複製代碼

源碼清楚地說明了NEW狀態就是一個線程剛剛被建立,可是尚未啓動地時候所處的狀態,這個和咱們上一小節中地NEW階段可以對應起來這裏就很少說了。

(2)RUNNABLE狀態 = RUNNABLE階段+RUNNING階段

* <li>{@link #RUNNABLE}<br>
     *     A thread executing in the Java virtual machine is in this state.
     *     </li>
複製代碼

這段的說明意思是在java虛擬機中執行的線程所處的狀態稱之爲RUNNABLE。 也就是說咱們上一節中分開講解的RUNNABLE階段RUNNING階段在線程狀態中來看統一都稱之爲RUNNABLE狀態,之因此咱們在生命週期劃分的時候把這兩個狀態拆分開來看是由於這兩個狀態差異仍是很大的,RUNNING狀態的線程比RUNNABLE狀態的線程更加複雜一些。

(3)BLOCKED狀態+WAITING狀態+TIMED_WAITING狀態 = BLOCKED階段

* <li>{@link #BLOCKED}<br>
     *     A thread that is blocked waiting for a monitor lock
     *     is in this state.
     *     </li>
     * <li>{@link #WAITING}<br>
     *     A thread that is waiting indefinitely for another thread to
     *     perform a particular action is in this state.
     *     </li>
     * <li>{@link #TIMED_WAITING}<br>
     *     A thread that is waiting for another thread to perform an action
     *     for up to a specified waiting time is in this state.
     *     </li>
複製代碼

源碼中對於這三個狀態對敘述是這樣的

BLOCKED:等待獲取監視器鎖而進入阻塞隊列的線程所處的狀態

WAITING:無限期等待另外一線程執行特定操做的線程處於此狀態。

TIMED_WAITING:等待另外一線程執行操做的線程在指定的等待時間內處於此狀態。

同窗們對於BLOCKEDWAITING這兩個狀態的區別清楚嗎?清楚的直接進入(4)哦~

咱們仍是經過源碼中對於state枚舉值的描述來進入主題。 state中對於BLOCKED狀態的描述以下:

/**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED
複製代碼

源碼中對於BLOCKED這一狀態的敘述是這樣的,線程在等待獲取一個monitor lock的時候該線程就是處於BLOCKED。說通俗一點就是被synchronized關鍵字描述的同步塊或者方法,此時其餘線程想要獲取這個被描述的同步塊或者方法的時候,這個線程就會進去等待獲取monitor lock的時候,也就是進入來BLOCKED狀態。這裏的關鍵是monitor lock監視鎖。

喚醒方式是目標監視鎖monitor lock主動釋放,這時候咱們去競爭這個監視鎖併成功以後。

state中對於WAITING狀態的描述以下:

/**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING
複製代碼

這裏給咱們描述了哪些狀況下咱們的線程會進入WAITING狀態,分別是調用Object.waitThread.joinLockSupport.park三個API接口才會進入WAITING狀態,這邊對於線程API不作具體闡述,後面會單獨開一章來介紹,咱們這邊就先不糾結。

也說明了WAITING狀態是當一個線程處於等待另外一個線程完成一個特定操做時候所處的狀態,這裏的關鍵是兩個線程,目標線程等待另外一個線程完成某個動做

喚醒方式是等待其餘線程完成本身邏輯以後,調用notify或者notiffyall喚醒處於WAITING狀態的線程。

(4)TERMINATED狀態 = TERMINATED狀態


到這裏同窗們應該已經清楚了線程生命週期以及各個階段之間的轉換,同時明白了不一樣階段的生命週期所對應的java線程狀態。接下來是本章第二段源碼分析啦~

劃重點啦,看累的同窗休息休息,接下來是start()源碼分析

3、Thread.start()設計模式源碼解讀

開始以前咱們先思考一下,爲何start()能啓動咱們的線程,run()不行呢?

廢話不說,先上源碼

/**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
複製代碼

老套路,咱們先看方法說明

/**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
複製代碼

翻譯start()方法使線程開始運行,java虛擬機會調用run()方法來執行線程的邏輯單元。而且線程只容許啓動一次,屢次啓動線程是不合法的,會拋出IllegalThreadStateException異常。

下面咱們經過三步來解讀Thread.start()

(1) 判斷threadStatus值

/**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
複製代碼

threadStatus==0 表示NEW狀態,若是在調用Thread.start()到時候發現threadStatus!=0那麼表示線程已經不處於NEW狀態了,此時調用該方法是不合法的,因此拋出IllegalThreadStateException異常。(這麼簡單的邏輯判斷,相信同窗們必定和我同樣生出了一個想法「我上我也行😁」)

(2) 加入線程組

/* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */
        group.add(this);
複製代碼

翻譯:通知組新線程已經被啓動,須要添加到線程組中,同時減小未開始計數。 看完翻譯,不知所云,咱們繼續追蹤add

void add(Thread t) {
        synchronized (this) {
            if (destroyed) {
                throw new IllegalThreadStateException();
            }
            if (threads == null) {
                threads = new Thread[4];
            } else if (nthreads == threads.length) {
                threads = Arrays.copyOf(threads, nthreads * 2);
            }
            threads[nthreads] = t;

            // This is done last so it doesn't matter in case the // thread is killed nthreads++; // The thread is now a fully fledged member of the group, even // though it may, or may not, have been started yet. It will prevent // the group from being destroyed so the unstarted Threads count is // decremented. nUnstartedThreads--; } } 複製代碼

add方法咱們也分爲三小步來看

第一步: 判斷線程組是否已經被銷燬了,若是線程組已經不存在了,那就拋出IllegalThreadStateException異常。

if (destroyed) {
                throw new IllegalThreadStateException();
            }
複製代碼

第二步:把線程加入到線程組數組中。

if (threads == null) {
                threads = new Thread[4];
            } else if (nthreads == threads.length) {
                threads = Arrays.copyOf(threads, nthreads * 2);
            }
            threads[nthreads] = t;

            // This is done last so it doesn't matter in case the // thread is killed nthreads++; 複製代碼

首先判斷threads是否爲空,爲空則建立一個數組,初始化長度爲4。若是threads不爲空,判斷nthreads == threads.length若是爲true,則對threads數組進行擴容threads = Arrays.copyOf(threads, nthreads * 2),把入參添加到threads[]中,對下標進行累加nthreads++

第三步:未開始線程計數減一

// The thread is now a fully fledged member of the group, even
            // though it may, or may not, have been started yet. It will prevent
            // the group from being destroyed so the unstarted Threads count is
            // decremented.
            nUnstartedThreads--;
複製代碼

(3) 調用start0()方法啓動線程

boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
        
複製代碼

started是線程的啓動狀態,預設爲false,啓動成功再賦值爲true。而後調用start0()方法

private native void start0();
複製代碼

至此咱們回想一下咱們的線程執行邏輯單元是寫在run()中的,那麼全程發現沒有地方在調用run(),只有這個start0()方法最爲可疑。能夠看到這個方法是一個本地方法,在Thread.java類定義的開頭有靜態塊來完成註冊。

/* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
複製代碼

好的到此打住,以後會涉及到JVM底層的一些內容,這裏咱們就不作擴展,咱們只要知道start()內部調用調用start0()方法,最終是在建立完咱們的線程以後,在線程內部調用來run()方法。

咱們只要記清楚一點,真正建立線程和調用run方法的地方是由jvm來完成的,咱們這裏只是做爲一個啓動發起方完成一個發送線程啓動信號的動做就行。咱們這裏再也不帶你們去一堆CPP文件裏面去翻雲覆雨了,以避免暈車。

到這裏咱們應該清楚了本節開始時候的問題了,爲何start()能啓動咱們的線程,run()不行呢。相信你們已經知道了緣由了,由於run只能做爲是一個內部實現類,若是單純調用run方法的話,咱們只是在本線程中實現了邏輯而並無真正地開闢出一個新的線程來執行咱們的邏輯。開闢新的線程須要調用start0()來讓JVM幫咱們完成建立,因此只有調用start()方法才能啓動線程。

這裏我也寫了一個小例子來驗證:

public static class TestStartAndRun implements Runnable{

    @Override
    public void run() {

      System.out.println("個人名字叫"+Thread.currentThread().getName());
    }
  }


  public static void main(String[] args) {

    new Thread(new TestStartAndRun(),"start").start();

    new Thread(new TestStartAndRun(),"run").run();

  }
複製代碼

輸出:

個人名字叫start
個人名字叫main
複製代碼

能夠看到,咱們定義的名稱叫作run的線程並無啓動起來,執行Thread.run()的是咱們的main線程,這就證實來run不能建立線程,可是能在當前線程中執行內部類run的邏輯單元,而且能夠執行屢次。

4、擴展閱讀——模仿start的模版設計模式本身寫一個相似的程序

/**
   * 杯子
   */
  final void Teacup(String nothing){
    System.out.print("今天");
    Brewing(nothing);
    System.out.println(",愉快的一天開始啦!");
  }

  /**
   * 泡點什麼呢
   * @param nothing
   */
  void Brewing(String nothing){
  }

  public static void main(String[] args) {
    TemplateDesignExample t1 = new TemplateDesignExample(){
      @Override
      void Brewing(String nothing){
        System.out.print("下雨,"+nothing);
      }
    };
    t1.Teacup("早上喝咖啡");


    TemplateDesignExample t2 = new TemplateDesignExample(){
      @Override
      void Brewing(String nothing){
        System.out.print("晴天,"+nothing);
      }
    };
    t2.Teacup("早上喝茶");
  }
複製代碼

輸出:

今天下雨,早上喝咖啡,愉快的一天開始啦!
今天晴天,早上喝茶,愉快的一天開始啦!
複製代碼

這樣不論是下雨仍是晴天,咱們均可以喝到本身想喝的,每一天均可以愉快地編碼,若是能點點關注,點點贊👍,每一天都是雙倍的快樂哦!

祝同窗們端午小長假快樂呀,放假也不要忘記和我一塊兒學習哦~

相關文章
相關標籤/搜索