?

13260580922

87538126

一篇文章弄懂Java多線程基礎和Java內存模型

2020-05-30 09:21:03 作者:文章來源于網絡
一、多線程的生命周期及五種基本狀態
Java多線程生命周期,首先看下面這張經典的圖,圖中基本上囊括了Java中多線程重要知識點。
Java線程具有五中基本狀態
 
新建狀態(New):當線程對象對創建后,即進入了新建狀態,如:Thread t = new MyThread();
 
就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處于就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,并不是說執行了t.start()此線程立即就會執行;
 
運行狀態(Running):當CPU開始調度處于就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處于就緒狀態中;
 
阻塞狀態(Blocked):處于運行狀態中的線程由于某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:
 
1.等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;
 
2.同步阻塞 – 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態;
 
3.其他阻塞 – 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
 
死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
 
二、Java多線程的創建及啟動
Java中線程的創建常見有如三種基本形式
 
1.繼承Thread類,重寫該類的run()方法
繼承Thread類,重寫該類的run()方法
 
運行結果:
 
這是代碼運行后的結果,從圖中可以看出:
 
1、有三個線程:main、Thread-0 、Thread-1
 
2、Thread-0 、Thread-1兩個線程輸出的成員變量 i 的值不連續(這里的 i 是實例變量而不是局部變量)。因為:通過繼承Thread類實現多線程時,每個線程的創建都要創建不同的子類對象,導致Thread-0 、Thread-1兩個線程不能共享成員變量 i ;
 
3、線程的執行是搶占式,并沒有說Thread-0 或者Thread-1一直占用CPU(這也與線程優先級有關,這里Thread-0 、Thread-1線程優先級相同,關于線程優先級的知識這里不做展開)
 
2.通過實現Runnable接口創建線程類
定義一個類實現Runnable接口;創建該類的實例對象obj;將obj作為構造器參數傳入Thread類實例對象,這個對象才是真正的線程對象
 
運行結果:
 
1、線程1和線程2輸出的成員變量i是連續的,也就是說通過這種方式創建線程,可以使多線程共享線程類的實例變量,因為這里的多個線程都使用了同一個target實例變量。但是,當你使用我上述的代碼運行的時候,你會發現,其實結果有些并不連續,這是因為多個線程訪問同一資源時,如果資源沒有加鎖,那么會出現線程安全問題(這是線程同步的知識,這里不展開);
2、java8 可以使用lambda方式創建多線程。
 
3.通過Callable和Future接口創建線程
創建Callable接口實現類,并實現call()方法,該方法將作為線程執行體,且該方法有返回值,再創建Callable實現類的實例;使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值;使用FutureTask對象作為Thread對象的target創建并啟動新線程;調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值
 
call()方法的返回值類型與創建FutureTask對象時<>里的類型一致。
 
三、Java內存模型概念
在并發編程中,我們需要處理兩個關鍵問題:線程之間如何通信及線程之間如何同步(這里的線程是指并發執行的活動實體)。通信是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。
 
在共享內存的并發模型里,線程之間共享程序的公共狀態,線程之間通過寫-讀內存中的公共狀態來隱式進行通信。在消息傳遞的并發模型里,線程之間沒有公共狀態,線程之間必須通過明確的發送消息來顯式進行通信。
 
堆內存在線程之間共享(本文使用“共享變量”這個術語代指實例域,靜態域和數組元素)。局部變量(Local variables),方法定義參數(java語言規范稱之為formal method parameters)和異常處理器參數(exception handler parameters)不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。
 
主內存和工作內存解釋
 
主內存(main memory): 類的實例所存在的區域,所有的實例都存在主存儲器內,并且實例的字段也位于這里。主存儲器為所有的線程所共享,主內存主要對應于Java堆中對象的實例數據部分。
 
工作內存(working memory): 每個線程各自獨立所擁有的作業區,在working memory中,存有main memory中的部分拷貝,稱之為工作拷貝(working copy)。
 
Java線程之間的通信由Java內存模型(本文簡稱為JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。Java內存模型的抽象示意圖如下:
 
從上圖來看,線程A與線程B之間如要通信的話,必須要經歷下面2個步驟:
 
首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
然后,線程B到主內存中去讀取線程A之前已更新過的共享變量。
下面通過示意圖來說明這兩個步驟:
如上圖所示,本地內存A和B有主內存中共享變量x的副本。假設初始時,這三個內存中的x值都為0。
1、線程A在執行時,把更新后的x值(假設值為1)臨時存放在自己的本地內存A中。當線程A和線程B需要通信時,線程A首先會把自己本地內存中修改后的x值刷新到主內存中,此時主內存中的x值變為了1。
2、線程B到主內存中去讀取線程A更新后的x值,此時線程B的本地內存的x值也變為了1。
從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來為java程序員提供內存可見性保證。
 
四、內存間的交互操作
主內存與工作內存之間的交互操作定義了8種原子性操作。具體如下:
 
lock(鎖定):作用于主內存的變量,將一個變量標識為一條線程獨占狀態
 
unlock(解鎖):作用于主內存的變量,將一個處于鎖定狀態的變量釋放出來
 
read(讀取):作用于主內存的變量,把一個變量的值從主內存傳輸到線程的工作內存中
 
load(載入):作用于工作內存的變量,把read傳輸的變量值放入或者拷貝到工作內存的變量副本
 
use(使用):作用于工作內存的變量,表示線程引用工作內存中的變量值,將工作內存中的一個變量的值傳遞給執行引擎
 
assign(賦值):作用于工作內存的變量,表示線程將指定的值賦值給工作內存中的某個變量。
 
store(存儲):作用于工作內存的變量,把工作內存中的一個變量的值傳送給主內存中
 
write(寫入):作用于主內存的變量,將store傳遞的變量值放入到主內存中對應的變量里
 
下面圖片能幫我們加深印象
 
五、volatile和synchronized的區別
首先需要理解線程安全的兩個方面:執行控制和內存可見。
 
執行控制的目的是控制代碼執行(順序)及是否可以并發執行。
 
內存可見控制的是線程執行結果在內存中對其它線程的可見性。根據Java內存模型的實現,線程在具體執行時,會先拷貝主存數據到線程本地(CPU緩存),操作完成后再把結果從線程本地刷到主存。
 
synchronized關鍵字解決的是執行控制的問題,它會阻止其它線程獲取當前對象的監控鎖,這樣就使得當前對象中被synchronized關鍵字保護的代碼塊無法被其它線程訪問,也就無法并發執行。更重要的是,synchronized還會創建一個內存屏障,內存屏障指令保證了所有CPU操作結果都會直接刷到主存中,從而保證了操作的內存可見性,同時也使得先獲得這個鎖的線程的所有操作,都happens-before于隨后獲得這個鎖的線程的操作。
 
volatile關鍵字解決的是內存可見性的問題,會使得所有對volatile變量的讀寫都會直接刷到主存,即保證了變量的可見性。這樣就能滿足一些對變量可見性有要求而對讀取順序沒有要求的需求。
 
使用volatile關鍵字僅能實現對原始變量(如boolen、 short 、int 、long等)操作的原子性,但需要特別注意, volatile不能保證復合操作的原子性。
 
對于volatile關鍵字,當且僅當滿足以下所有條件時可使用:
 
對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
該變量沒有包含在具有其他變量的不變式中
volatile和synchronized的區別
 
volatile本質是在告訴jvm當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取; synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。
volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的
volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性
volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。
volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化。
 
?
QQ在線咨詢
咨詢熱線
13260580922
報名電話
87538126
浙江11选5_官网