深刻理解Java併發框架AQS系列(一):線程

1、概述
java

1.一、前言

重劍無鋒,大巧不工api

讀j.u.c包下的源碼,永遠沒法繞開的經典併發框架AQS,其設計之精妙堪比一件藝術品,令衆多學者絕不吝惜溢美之詞。近期準備出一系列關於AQS的文章,系統的來說解AQS,我將跟你們一塊兒帶着敬畏之心去讀她,但也會對關鍵部分提出質疑及思考安全

原本打算直接以闡述鎖概念做爲開頭,但發現始終都繞不過線程這關,再加上如今好多講述線程的文章概念混淆不清,誤人子弟,索性開此文,一來作一些基礎工做的鋪墊,二來咱們把線程的一些概念聊透併發

1.二、名詞釋義

名詞 描述
j.u.c 本文特指java.util.concurrent包
AQS 本文特指圍繞j.u.c包下的類AbstractQueuedSynchronizer.java提供的一套輕量級併發框架
2、線程狀態

線程狀態屬於老生常談的話題,在網上一搜一大把,但發現不少文章都是人云亦云。咱們將結合代碼實例來逐一論述線程狀態。框架

我嘗試想用一張圖把狀態流轉描述清楚,發現很是困難,因爲wait/notify使用的特殊性,會將整個流程圖攪得很亂,因此此處咱們把狀態流轉拆分爲(非wait方法)及(wait方法)。若是你在某些文章中看到用一張圖來描述線程狀態流轉的,那麼要留心了,仔細甄別下,看其是否遺漏了某些場景ide

站在JVM的視角,將線程狀態分紅了6種狀態:this

  • NEW-初始
  • RUNNABLE-可運行
  • BLOCKED-阻塞
  • WAITING-等待
  • TIMED_WAITING-超時等待
  • TERMINATED-結束

爲了論述的更爲完全,咱們站在操做系統的角度,將RUNNABLE-可運行狀態拆分爲runnable-就緒狀態及running-運行狀態,故一共7種狀態操作系統

2.一、狀態定義

2.1.一、初始狀態(new)

線程在新建後,且在調用start方法前的狀態爲初始狀態,此時操做系統感知不到線程的存在,僅存在於JVM內部線程

2.1.二、就緒狀態(runnable)

就緒狀態表示當前線程已經啓動,只要操做系統調度了cpu時間片,便可運行,其本質上仍是處於等待;例如3個正常啓動且無阻塞的線程,運行在一個2核的計算機上,那麼在某一個時刻,必定至少有1個線程處於就緒狀態,等待着cpu資源設計

2.1.三、運行狀態(running)

惟一一個正在運行中的狀態,且當前線程沒有阻塞、休眠、掛起等;處於此狀態的線程,經過主動調用Thread.yield()方法,可變爲就緒狀態

2.1.四、阻塞狀態(blocked)

線程被動地處於synchronized的阻塞隊列中,沒有超時概念、不響應中斷

2.1.五、等待狀態(waiting)

顧名思義,線程處於主動等待中,且響應中斷;當線程主動調用瞭如下3個方法時,即處於等待狀態,等待其餘線程的喚起

  • Thread.join()
  • LockSupport.park()
  • Object.wait()

與阻塞狀態的區別:

  • 阻塞狀態:線程老是被動的處於阻塞狀態,當一個線程執行synchronized代碼塊時,它不知道本身立刻搶到鎖並執行後續邏輯仍是會被阻塞
  • 等待狀態:線程很清楚本身接下來要處於等待狀態,並且這個命令是線程本身發起的,即使什麼時候被喚醒它沒法控制

2.1.六、超時等待狀態(timed_waiting)

此狀態與waiting狀態定義基本一致,只是引入了超時概念;進入timed_waiting的方法以下:

  • Thread.sleep(long)
  • Thread.join(long)
  • LockSupport.parkNanos(long)
  • LockSupport.parkUntil(long)
  • Object.wait(long)

2.1.七、終止狀態(terminated)

線程運行完畢,處於此狀態的線程不能再次啓動,也不能轉換爲其餘狀態,等待垃圾回收

2.二、狀態流轉

初始 -> 就緒

線程調用Thread.start()方法便可進入就緒狀態

就緒 -> 運行

操做系統調度,JVM層面沒法干預

運行 -> 就緒

分主動、被動2種方式

  • 一、當前線程的cpu時間片用完,被動進入就緒狀態
  • 二、主動調用Thread.yield()

運行 -> 阻塞

2種場景可將一個運行狀態的線程變爲阻塞狀態,且都與synchronized相關

  • 場景1:線程因爭搶synchronized鎖失敗,從而進入等待隊列時,線程狀態置爲blocked

    @Test
    public void test5() throws Exception {
        Object obj = new Object();
        Thread thread1 = new Thread(() - > {
            synchronized(obj) {
                int sum = 0;
                // 模擬線程運行
                while(1 == 1) {
                    sum++;
                }
            }
        });
        thread1.start();
        // 停頓1秒鐘後再啓動線程2,保證線程1已啓動運行
        Thread.sleep(1000);
        Thread thread2 = new Thread(() - > {
            synchronized(obj) {
                System.out.println("進入鎖中");
            }
        });
        thread2.start();
        System.out.println("線程1狀態:" + thread1.getState());
        System.out.println("線程2狀態:" + thread2.getState());
    }
    
    ----------運行結果----------
    線程1狀態:RUNNABLE
    線程2狀態:BLOCKED
  • 場景2:處於Object.wait()的線程在被喚醒後,不會當即去執行後續代碼,並且是會從新爭搶synchronized鎖,爭搶失敗的即會進入同步隊列排序,此時的線程狀態一樣爲blocked

    @Test
    public void test6() throws Exception {
        Object obj = new Object();
        Thread[] threads = new Thread[2];
        for(int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() - > {
                synchronized(obj) {
                    try {
                        obj.wait();
                        // 模擬後續運算,線程不會立刻結束
                        while(1 == 1) {}
                    } catch(InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].setName("線程" + (i + 1));
            threads[i].start();
        }
        Thread.sleep(1000);
        // 激活全部阻塞線程
        synchronized(obj) {
            obj.notifyAll();
        }
        Thread.sleep(1000);
        System.out.println("線程1狀態:" + threads[0].getState());
        System.out.println("線程2狀態:" + threads[1].getState());
    }
    
    
    ----------運行結果----------
    線程1狀態:BLOCKED
    線程2狀態:RUNNABLE

運行 -> 等待

  • 場景1:調用Thread.join()

    @Test
    public void test7() throws Exception {
        Thread thread1 = new Thread(() - > {
            // 死循環,模擬運行
            while(1 == 1) {}
        });
        thread1.start();
        Thread thread2 = new Thread(() - > {
            try {
                thread1.join();
                System.out.println("線程2開始執行");
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread2.start();
        Thread.sleep(1000);
        System.out.println("線程2狀態:" + thread2.getState());
    }
    
    ----------運行結果----------
    線程2狀態:WAITING
  • 場景2:調用LockSupport.park(),即掛起線程,且只能掛起當前線程

    @Test
    public void test8() throws Exception {
        Thread thread1 = new Thread(LockSupport::park);
        thread1.start();
        Thread.sleep(1000);
        System.out.println("線程1狀態:" + thread1.getState());
    }
    
    ----------運行結果----------
    線程1狀態:WAITING

運行 -> 超時等待

  • 一、Thread.sleep(long)
  • 二、Thread.join(long)
  • 三、LockSupport.parkNanos(long)
  • 四、LockSupport.parkUntil(long)

讀者可自行寫代碼驗證,此處再也不贅述

等待/超時等待 -> 阻塞

當執行完Object.wait()/Object.wait(long)後,不會立刻進入就緒狀態,線程間還要繼續爭搶同步隊列的鎖,爭搶失敗的便會進入阻塞狀態;在AQS後續的條件隊列Condition文章中,還會繼續說明

運行 -> 終止

線程正常執行完畢,結束了run方法後便進入終止狀態,沒法再被喚起,等待GC回收

3、線程概念

3.一、曲折中前進

從線程api那些被@Deprecated標記的方法就能看出,線程的設計發展不是一路順風的,那些被標記過期的方法都帶來了哪些問題?咱們舉兩個例子來講明

3.1.一、Thread.stop()

這個方法不就是將線程停掉麼,能帶來什麼問題?並且調用此方法後,即使獲取了synchronized鎖也會自動釋放,咱們要掛起線程的時候,不也要調用LockSupport.park()方法麼

的確,其實萬惡之源在於stop()方法可由其餘線程調用,其餘線程在調用時,不知道目標線程是什麼狀態,也不知道其是否加鎖,或正在執行一些原子操做。

最直接的是會帶來2個問題,且都是災難級別的

3.1.1.一、程序原子性

例如:

public class MyThread extends Thread {
    private int i = 0;
    private int j = 0;
    @Override
    public void run() {
        synchronized(this) {
            ++i;
            try {
                //休眠10秒,模擬耗時操做
                Thread.sleep(10000);
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
            ++j;
        }
    }
    public void print() {
        System.out.println("i=" + i + " j=" + j);
    }
}

咱們必定認爲synchronized方法中的邏輯是原子操做,即全部線程都塵埃落定後,i與j的值必定相等;然而事與願違,因爲stop()的介入,破壞了程序的完整性

其次若是目標線程正在修改某個線程共享變量 ,stop()從天而降,這個共享變量最終形態誰也沒法預測,爲何會變成這樣,全部線程都大眼瞪小眼;就比如把一頭獅子放進澡堂洗澡,出來的時候變成了一隻雞,誰都沒法解釋,程序也即進入了混亂

3.1.1.二、沒法完全釋放的鎖

語言層面的鎖synchronized在執行stop()方法時會被釋放,但j.u.c下或自定義鎖就沒那麼好運了

@Test
public void test10() throws Exception {
    ReentrantLock reentrantLock = new ReentrantLock();
    Thread thread1 = new Thread(() - > {
        reentrantLock.lock();
        try {
            Thread.sleep(1000000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        reentrantLock.unlock();
    });
    thread1.start();
    Thread.sleep(500);
    System.out.println("thread1 狀態:" + thread1.getState());
    thread1.stop();
    // 等待線程1結束
    while(thread1.getState() != Thread.State.TERMINATED) {}
    System.out.println("主線程嘗試獲取鎖");
    reentrantLock.lock();
    System.out.println("主線程拿到了鎖");
}


----------運行結果----------
thread1 狀態:TIMED_WAITING
主線程嘗試獲取鎖

咱們看到目標鎖永遠沒法再進入

3.1.二、Thread.suspend() / Thread.resume()

從字面意思能夠看出,這2個方法是成對兒出現的

  • Thread.suspend()線程暫停
  • Thread.resume()線程恢復

它們帶來的了那個臭名昭著的問題:死鎖

@Test
public void test11() throws Exception {
    Object lock = new Object();
    Thread thread1 = new Thread(() - > {
        synchronized(lock) {
            try {
                Thread.sleep(2000000);
            } catch(InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("執行 finally");
            }
        }
    });
    thread1.start();
    Thread.sleep(500);
    thread1.suspend();
    System.out.println("已經將線程1暫停");
    System.out.println("準備獲取lock鎖");
    synchronized(lock) {
        System.out.println("主搶到鎖了");
    }
}


----------運行結果----------
已經將線程1暫停
準備獲取lock鎖

上述程序陷入了無盡的等待;由於目標線程雖然已經被suspend,但並不會釋放鎖,當主線程去嘗試加鎖時,便陷入了無盡等待

3.1.三、思考

爲何會產生這樣的現象?其實終其緣由是由於其餘線程在沒法得知目標線程運行狀態的前提下,強制進行kill或暫停,所帶來的一系列問題;舉個不恰當的例子:張三經過小推車持續搬磚了2個小時,工頭在辦公室經過傳呼下達命令:中止工做!此時張三當即放下手中的活兒,小推車因被張三佔用,其餘人沒法開戰工做。因此咱們是否應該去提醒,而不是直接下達命令,至於在什麼時間、什麼地點中止工做由張三來決定呢?這就引出了咱們要聊得下一個話題:中斷

3.二、線程中斷

線程中斷並非將一個正在運行的線程中斷而導致其終止;

線程中斷僅僅是設置線程的中斷標記位,不會對目標線程的運行產生干擾。而只有當目標線程響應了中斷,從而自發的拋出異常或結束waiting;

後續文章中將講到的AQS提供的方法都是支持響應中斷的,此處咱們簡單羅列一下經常使用的響應線程中斷的方法

  • Object.wait() / Object.wait(long)
  • Thread.join() / Thread.join(long)
  • Thread.sleep(long)
  • LockSupport.park() / LockSupport.parkNanos(long) / LockSupport.parkUntil(long)

那麼JVM內部是如何實現響應中斷呢?拿Thread.sleep(long)舉例,看其C++源碼會發現,JVM會將一次長睡眠分割爲屢次小的睡眠,目標就是及時響應中斷

咱們延續3.1小節的例子:張三經過小推車持續搬磚了2個小時,妻子看到後說「喝口水,歇會兒吧」(發送打斷命令),此時張三的反應可分爲如下2類:

  • 感受不累,繼續工做(不響應中斷)
  • 把東西歸置完畢、小推車歸還後,開始休息(讓出資源,並在合適的時機休息)

3.三、線程阻塞與掛起

主要討論wait/notify與park/unpark,二者既然都支持線程的掛起及激活,有什麼異同點嗎?各自的應用場景何在?

  • 相同點

    • 二者都實現現成掛起、喚醒功能,且支持超時等待、響應中斷
  • 不一樣點

    功能點 精準控制 執行順序 中斷
    wait/notify 掛起:指定當前線程掛起

    喚醒:隨機喚醒 1 個線程或所有喚醒
    執行順序須要嚴格保證wait操做發生在notify以前,若是notify在wait以前執行了,那麼wait操做將進入無限等待的窘境 響應中斷,且需處理編譯期異常
    park/unpark 掛起:指定當前線程掛起

    喚醒:精確喚醒指定的 1 個線程

    注:雖然喚醒可指定某線程,但掛起操做只會針對當前線程生效,由於當前線程並不瞭解被掛起線程的真實狀態,若是一旦可操控,勢必會帶來不可預期的安全問題
    unpark操做可發生在park以前,但僅會生效一次;例如針對線程A首先執行了2次unpark操做,而後對A第1次執行park操做時不會有阻塞,但第2次執行park時會進入等待 響應中斷,但不拋出異常,發生中斷後,park()方法會自動結束,經過Thread.interrupted()來判斷是中斷仍是unpark()致使的
相關文章
相關標籤/搜索