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


深入理解Java並發框架AQS系列(一):線程
深入理解Java並發框架AQS系列(二):AQS框架簡介及鎖概念
深入理解Java並發框架AQS系列(三):獨占鎖(Exclusive Lock)
深入理解Java並發框架AQS系列(四):共享鎖(Shared Lock)
深入理解Java並發框架AQS系列(五):條件隊列(Condition)

一、概述

1.1、前言

重劍無鋒,大巧不工

j.u.c包下的源碼,永遠無法繞開的經典並發框架AQS,其設計之精妙堪比一件藝術品,令眾多學者毫不吝惜溢美之詞。近期准備出一系列關於AQS的文章,系統的來講解AQS,我將跟大家一起帶着敬畏之心去讀她,但也會對關鍵部分提出質疑及思考

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

1.2、名詞釋義

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

二、線程狀態

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

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

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

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

為了論述的更為徹底,我們站在操作系統的角度,將RUNNABLE-可運行狀態拆分為runnable-就緒狀態running-運行狀態,故一共7種狀態

2.1、狀態定義

2.1.1、初始狀態(new)

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

2.1.2、就緒狀態(runnable)

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

2.1.3、運行狀態(running)

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

2.1.4、阻塞狀態(blocked)

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

2.1.5、等待狀態(waiting)

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

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

與阻塞狀態的區別:

  • 阻塞狀態:線程總是被動的處於阻塞狀態,當一個線程執行synchronized代碼塊時,它不知道自己馬上搶到鎖並執行后續邏輯還是會被阻塞
  • 等待狀態:線程很清楚自己接下來要處於等待狀態,而且這個命令是線程自己發起的,即便何時被喚醒它無法控制

2.1.6、超時等待狀態(timed_waiting)

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

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

2.1.7、終止狀態(terminated)

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

2.2、狀態流轉

初始 -> 就緒

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

就緒 -> 運行

操作系統調度,JVM層面無法干預

運行 -> 就緒

分主動、被動2種方式

  • 1、當前線程的cpu時間片用完,被動進入就緒狀態
  • 2、主動調用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
    

運行 -> 超時等待

  • 1、Thread.sleep(long)
  • 2、Thread.join(long)
  • 3、LockSupport.parkNanos(long)
  • 4、LockSupport.parkUntil(long)

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

等待/超時等待 -> 阻塞

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

運行 -> 終止

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

三、線程概念

3.1、曲折中前進

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

3.1.1、Thread.stop()

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

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

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

3.1.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方法中的邏輯是原子操作,即所有線程都塵埃落定后,ij的值一定相等;然而事與願違,由於stop()的介入,破壞了程序的完整性

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

3.1.1.2、無法徹底釋放的鎖

語言層面的鎖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.2、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.3、思考

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

3.2、線程中斷

線程中斷並不是將一個正在運行的線程中斷而致使其終止;

線程中斷僅僅是設置線程的中斷標記位,不會對目標線程的運行產生干擾。而只有當目標線程響應了中斷,從而自發的拋出異常或結束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.3、線程阻塞與掛起

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

  • 相同點

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

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

    喚醒:隨機喚醒 1 個線程或全部喚醒
    執行順序需要嚴格保證wait操作發生在notify之前,如果notifywait之前執行了,那么wait操作將進入無限等待的窘境 響應中斷,且需處理編譯期異常
    park/unpark 掛起:指定當前線程掛起

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

    注:雖然喚醒可指定某線程,但掛起操作只會針對當前線程生效,因為當前線程並不了解被掛起線程的真實狀態,如果一旦可操控,勢必會帶來不可預期的安全問題
    unpark操作可發生在park之前,但僅會生效一次;例如針對線程A首先執行了2次unpark操作,然后對A第1次執行park操作時不會有阻塞,但第2次執行park時會進入等待 響應中斷,但不拋出異常,發生中斷后,park()方法會自動結束,通過Thread.interrupted()來判斷是中斷還是unpark()導致的


免責聲明!

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



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