你必須應(yīng)該掌握的Java并發(fā)基礎(chǔ)

前情引入

?在工作中,我們或多或少都會接觸到與線程相關(guān)的東西,比如線程池、比如Runnable、Callable接口等等。如果這些你都沒有接觸過,至少Java程序的入口——main方法你一定有所了解。因?yàn)樵贘ava程序的運(yùn)行過程中,承載main方法的線程叫主線程。

?本篇中,我們不會做過多的代碼演示,我們的目的僅僅是了解Java并發(fā)相關(guān)的概念,以及掌握幾個相關(guān)的常見題目。

何為線程,何為進(jìn)程

?在開始探究線程之前,我們應(yīng)該先從應(yīng)用程序的角度去理解一下什么叫進(jìn)程。

?簡單的說,進(jìn)程是一段程序的一次執(zhí)行過程,是系統(tǒng)運(yùn)行程序的基本單位。在Java中,當(dāng)我們啟動main方法時就開啟了一個jvm的進(jìn)程,而這個main方法所在的線程就是這個進(jìn)程中的其中一個線程。當(dāng)我們在運(yùn)行一段代碼的時候,可以在任務(wù)管理器中看到相應(yīng)的進(jìn)程信息。
?譬如,在執(zhí)行這樣一段代碼時。

    public class ClinitTest {
        public static void main(String[] args){
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()){
                int currentNumber = scanner.nextInt();
                // 以0結(jié)束輸入
                if(currentNumber == 0){
                    break;
                }
            }
        }
    }

?上面的代碼片段并沒有什么實(shí)際意義,if()語塊的作用僅僅是讓程序不要一瞬即過,這樣方便我們觀察這個進(jìn)程。
?通過jps命令可查看機(jī)器上有哪些Java的進(jìn)程在運(yùn)行,并且可看到這些進(jìn)程的PID。如下圖:


機(jī)器上運(yùn)行中的Java進(jìn)程

?我們可以通過進(jìn)程的PID,在任務(wù)管理器中查找到這個進(jìn)程,如下:


任務(wù)管理器中ClinitTest所在進(jìn)程

?事實(shí)上,線程和進(jìn)程很相似,但線程是比進(jìn)程更小的執(zhí)行單位。但我們可以這樣去區(qū)分線程和進(jìn)程:一個虛擬機(jī)可以產(chǎn)生多個進(jìn)程,而一個進(jìn)程可以持有多個線程。
?或者從main方法來談這個問題。我們常說一切事物都是有因才有果,一個main方法想要被執(zhí)行,就必須要有線程來承載這個方法;一個線程想要被執(zhí)行,就需要有進(jìn)程來承載;只不過jvm作為這個小世界的創(chuàng)造者,對于進(jìn)程和線程就有統(tǒng)治權(quán)和調(diào)度權(quán)。
?除了上面的區(qū)別之外,進(jìn)程擁有獨(dú)立的運(yùn)行地址空間,各個進(jìn)程之間不會互相影響;而線程則不是,同類的多個線程可以共享同一個進(jìn)程的地址空間。

在jvm中哪些區(qū)域是線程共享的

?在討論這個問題之前,先讓我們看一下jvm的內(nèi)存模型(因jvm的版本不一樣,會存在差異)。此處的jvm內(nèi)存模型基于jdk1.8進(jìn)行簡要描述。如下圖所示:


jvm內(nèi)存模型(jdk1.8)

?按照該圖的描述,多個線程可共享堆和元空間及直接內(nèi)存的內(nèi)容,而每個線程有自己的程序計數(shù)器、本地方法棧,虛擬機(jī)棧。

?如果你對jvm的內(nèi)存模型并不了解,不知道每一個區(qū)域都存放了些什么樣的數(shù)據(jù),可在了解了相關(guān)內(nèi)容后再回到這里。關(guān)于JVM內(nèi)存模型的各個區(qū)域更加詳細(xì)的介紹可參見:此處應(yīng)插眼。
?如果你已經(jīng)對這塊內(nèi)容熟悉,請思考這樣一個問題:為什么程序計數(shù)器、本地方法棧和虛擬機(jī)棧都是線程私有的呢?能給出你的理解嗎?

程序計數(shù)器為什么會被設(shè)計為線程私有

?程序計數(shù)器在運(yùn)行時數(shù)據(jù)區(qū)域中只是一塊很小的內(nèi)存空間,每個線程有自己獨(dú)立的程序計數(shù)器。關(guān)于為什么被設(shè)計為私有可以用一句話來概括性的回答:線程切換后找到正確的執(zhí)行位置。

?Java虛擬機(jī)的多線程是通過線程輪流切換、享有處理器的時間片段的方式實(shí)現(xiàn)的。在任何一個時刻,處理器(對于多核處理器來說,這里指的是一個內(nèi)核)只會執(zhí)行一條線程中的指令。
?舉例來說,假設(shè)一個處理器現(xiàn)在正在工作中,有兩個線程分別為ThreadA和ThreadB,ThreadA里面有3條虛擬機(jī)字節(jié)碼指令:A1、A2、A3、ThreadB里面有3條虛擬機(jī)字節(jié)碼指令:B1、B2、B3。假設(shè)處理器的調(diào)度時間間隔是50ns,切換線程所需時間為5ns,從ThreadA開始執(zhí)行。那么將出現(xiàn)下面這樣的工作場景:

  1. 相對時間0~5ns之間:處理器切換線程為ThreadA,通過ThreadA的程序計數(shù)器中當(dāng)前執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址:比如A的指令地址;
  2. 5~55ns之間:執(zhí)行ThreadA的指令,這段時間內(nèi),執(zhí)行完了A2指令,準(zhǔn)備執(zhí)行A3指令;
  3. 55~60ns之間:保存ThreadA的A3指令的地址到程序計數(shù)器,切換線程讓ThreadB享有處理器;
  4. 以此類推......

?雖然上面的例子只是一個簡化后的處理流程,各個場景設(shè)定的嚴(yán)謹(jǐn)性也有待商榷,但它真實(shí)的反應(yīng)了多線程的工作原理。從上面的內(nèi)容我們可以明確的得知程序計數(shù)器有兩個作用:第一個是為了記錄當(dāng)前線程究竟執(zhí)行到哪一句指令了;第二個是為了在線程再次獲得處理器時間片的時候能夠準(zhǔn)確的定位到應(yīng)該執(zhí)行的指令。

虛擬機(jī)棧為什么被設(shè)計為線程私有

?簡單來說,每個Java方法在執(zhí)行期間都會創(chuàng)建一個棧幀,這個棧幀用于存放局部變量表、操作數(shù)棧、常量池的引用等信息。每一個方法從被調(diào)用進(jìn)入到執(zhí)行完畢就對應(yīng)著棧幀在虛擬機(jī)的棧中入棧和出棧的全過程。

?所以為了保證線程中的局部變量、操作數(shù)以及引用等不被其他線程訪問到,虛擬機(jī)棧被設(shè)計為線程私有的。
?上面提到的局部變量表實(shí)際上存放了在編譯器就可知的虛擬機(jī)基本數(shù)據(jù)類型(float、double、byte、short、int、long、char、boolean)和對象引用。

本地方法棧

?作用類似于虛擬機(jī)棧,不同的地方在于:虛擬機(jī)棧作用的對象是Java方法(可能用Java方法不太嚴(yán)謹(jǐn),就是字節(jié)碼所定義的方法);而本地方法棧作用對象是Native方法。
?虛擬機(jī)規(guī)范中并沒有明確指出Native方法應(yīng)該用什么語言、什么數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn),所以虛擬機(jī)可根據(jù)自己的情況實(shí)現(xiàn)Native方法。比如用C/C++或者其他什么語言無所謂,只要虛擬機(jī)能保證這個本地方法能實(shí)現(xiàn)要求的功能就ok。

?關(guān)于堆的更多內(nèi)容請參考jvm的內(nèi)存模型那一章節(jié),這里只關(guān)心如下幾點(diǎn)即可:

  1. 堆空間是線程共享的;
  2. 所有的(忽略在棧上分配的情況,對這塊感興趣可參考方法逃逸分析的相關(guān)技術(shù))對象實(shí)例都在這里分配內(nèi)存;

?堆為什么沒有被設(shè)計成線程私有?因?yàn)閷ο笫窃诙阎蟹峙涞膬?nèi)存,很多情況下我們需要在不同的線程中訪問同一個對象,所以堆是線程共享的區(qū)域。

元空間

?這一塊空間存儲的是元數(shù)據(jù),也就是類加載后的信息。在jdk1.7之前,這部分?jǐn)?shù)據(jù)被放在堆內(nèi)(具體是堆內(nèi)的方法區(qū)),在jdk1.8的時候被放到了直接內(nèi)存的管轄范圍之中,主要原因是大量的類加載信息經(jīng)常會導(dǎo)致這一塊區(qū)域溢出。

線程的生命周期

?《Java并發(fā)編程藝術(shù)》一書中有提到,Java線程在運(yùn)行的生命周期中的某一時刻,只能處于線程所允許的其中一個狀態(tài)。

線程的狀態(tài)有哪些

?Java中線程的狀態(tài)有初始狀態(tài)、運(yùn)行狀態(tài)、阻塞狀態(tài)、等待狀態(tài)、超時等待狀態(tài)和終止等待。詳見下表:

狀態(tài)名稱 說明
NEW 初始狀態(tài),線程被構(gòu)建,但是還沒有調(diào)用start()方法
RUNNABLE 運(yùn)行狀態(tài),操作系統(tǒng)中的'就緒'和'運(yùn)行'統(tǒng)稱為'運(yùn)行中'
BLOCKED 阻塞狀態(tài),該狀態(tài)說明線程正阻塞于鎖
WAITING 等待狀態(tài),表示線程正處于等待狀態(tài),等待其他線程做出一些特定的動作(通知或者中斷)
TIME_WAITING 超時等待狀態(tài),與WAITING的區(qū)別是,這個狀態(tài)可以在指定的時間范圍內(nèi)自行返回
TERMINATED 終止?fàn)顟B(tài),線程已被執(zhí)行完畢
線程的狀態(tài)如何轉(zhuǎn)換

?線程的狀態(tài)隨著代碼的執(zhí)行而切換,線程狀態(tài)的變遷如下圖所示:(摘自《Java并發(fā)編程藝術(shù)》)


線程狀態(tài)變遷圖

?從上圖可以看出,當(dāng)創(chuàng)建一個線程后,線程處于NEW狀態(tài);調(diào)用線程的start()方法后,線程將處于READY(就緒)狀態(tài);系統(tǒng)調(diào)度(獲得處理器的時間片)后,處于RUNNING(運(yùn)行中)狀態(tài);當(dāng)run()方法執(zhí)行完畢或者異常退出時,處于TERMINATED狀態(tài)。

?在Java中,READY和RUNNING狀態(tài)被統(tǒng)稱為RUNNABLE狀態(tài)。當(dāng)線程處于RUNNABLE時,可以和阻塞、等待和超時等待相互切換。上面的圖已經(jīng)有相關(guān)的描述,在此就不作過多的闡述了。

并發(fā)基礎(chǔ)部分的常見題

?根據(jù)上面的內(nèi)容,我們來看看這部分常常會被問到的幾個問題,重點(diǎn)在于該怎么回答。

(一)并發(fā)與并行的區(qū)別

?答:并發(fā)是指多個任務(wù)交替使用處理器,在某一個確定的時刻只有一個任務(wù)在使用處理器(對多核CPU來說,這里的處理器指的是具體的某一個內(nèi)核);而并行是指多個任務(wù)在任一時刻確實(shí)是同時在執(zhí)行的。

(二)什么叫線程的上下文切換

?答:處理器在任一時刻只能處理一個任務(wù),當(dāng)這個任務(wù)的時間片用完之后,會保存這個任務(wù)的當(dāng)前狀態(tài)然后把處理器讓給另外一個線程使用。從這個任務(wù)保存到再加載另外一個任務(wù)的過程就是一次上下文切換。

(三)說說sleep()方法和wait()方法區(qū)別和共同點(diǎn)

?答:關(guān)鍵記住一點(diǎn),sleep方法沒有釋放鎖,而wait方法釋放了鎖。因?yàn)閟leep方法被調(diào)用后仍然持有鎖,所以只能等方法執(zhí)行完后,線程自動蘇醒;而wait方法釋放了鎖,別的線程可以通過調(diào)用同一個對象上的notify()或者notifyAll()方法來喚醒它。

(四)為什么我們使用線程時調(diào)用的是start方法,為什么不是直接執(zhí)行run()方法

?答:new一個Thread,線程進(jìn)入NEW(初始)狀態(tài),想要被執(zhí)行就需要調(diào)用start方法。start方法會執(zhí)行相應(yīng)的準(zhǔn)備工作,并自動調(diào)用run()方法。如果直接調(diào)用run()方法會被當(dāng)成當(dāng)前線程的一個普通方法被執(zhí)行。

?Java所使用的線程模型是內(nèi)核級線程(KLT),start方法內(nèi)部會調(diào)用一個本地的start0()方法,這個start0方法會完成線程的創(chuàng)建和初始化,這些都是操作系統(tǒng)幫我們完成的。而run方法并沒有直接調(diào)用start0方法,所以這不是多線程的工作。關(guān)于這塊,建議大家看一下Thread類中的start方法和run方法源碼。


?本章我們已經(jīng)對Java的并發(fā)有了一個初步的認(rèn)識,在下一章中我們將對多線程所帶來的問題進(jìn)行分析,以及該如何去解決這些問題。


參考資料列表

1. 《Java并發(fā)編程藝術(shù)》——方騰飛,魏鵬,程曉明著


擴(kuò)展區(qū)域

擴(kuò)展區(qū)域主體

這是一個沒有實(shí)現(xiàn)的擴(kuò)展。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容