Java Thread 多線程

Java Thread 多線程

程序:是指令和數(shù)據(jù)的有序集合,本身沒有任何運行的含義,是一個靜態(tài)的概念

進程:是執(zhí)行程序的一次執(zhí)行過程,是一個動態(tài)的概念,是系統(tǒng)資源分配的單位

線程:一個進程可以包含若干個線程,一個進程至少有一個線程,不然沒有存在的意義,線程是CPU調(diào)度和執(zhí)行的單位。

線程創(chuàng)建

  1. 創(chuàng)建自定義線程類繼承Thread類

重寫run() 方法,編寫線程執(zhí)行體。創(chuàng)建線程對象,調(diào)用start() 方法啟動線程

  • 主線程如果調(diào)用run() 方法,則會先執(zhí)行run() 方法

  • 多條執(zhí)行路徑,主線程調(diào)用start() 方法,子線程就會調(diào)用run() 方法,主線程和子線程<u>交替</u>執(zhí)行(即同時進行,但在同一時間只能做一件事情)

注意:

線程開啟不一定立即執(zhí)行,由CPU調(diào)度執(zhí)行

<u>不建議使用:避免OOP單繼承局限性</u>

  1. 創(chuàng)建一個線程聲明實現(xiàn)Runnable接口

重寫run() 方法,編寫線程執(zhí)行體。創(chuàng)建線程對象,調(diào)用start() 方法啟動線程

在主線程里面創(chuàng)建Runnable接口的實現(xiàn)類對象

TestThread testThread = new TestThread(); //然后丟入創(chuàng)建的Threa類對象里
/*
Thread thread = new Thread(testThread); //創(chuàng)建線程對象,通過線程對象開啟線程
thread.start(); */
//注釋里兩行代碼相當于下行代碼
new Thread(testThread).start();

<u>推薦使用:避免單繼承局限性,靈活方便,方便同一個對象被多個對象使用。</u>

  1. 實現(xiàn)Callable 接口(了解)

實現(xiàn)Callable接口,需要返回值類型,重寫call() 方法,需要拋出異常,創(chuàng)建目標對象,<u>創(chuàng)建執(zhí)行服務</u> ExecutorService,通過服務去提交方法,最后關閉服務

好處:可以定義返回值,可以拋出異常

補充

用start方法來啟動線程,真正實現(xiàn)了多線程運行,這時無需等待run方法體中的代碼執(zhí)行完畢而直接繼續(xù)執(zhí)行后續(xù)的代碼。通過調(diào)用Thread類的 start()方法來啟動一個線程,這時此線程處于就緒(可運行)狀態(tài),并沒有運行,一旦得到cpu時間片,就開始執(zhí)行run()方法,這里的run()方法 稱為線程體,它包含了要執(zhí)行的這個線程的內(nèi)容,Run方法運行結束,此線程隨即終止。

靜態(tài)代理

靜態(tài)代理模式:

  • 真是對象和代理對象都要實現(xiàn)同一個接口
  • 代理對象要代理真實對象(實現(xiàn)真實對象做不了的事情,在代理對象里引入真實對象)

好處:

  • 代理對象可以做很多真實對象做不了的事情
  • 真是對象專注做自己的事情

Lambda表達式

new Thread( ()-> sout("nihao!") ).start();
  • 避免匿名內(nèi)部類定義過多
  • 讓代碼看起來更簡潔
  • 去掉一堆沒意義的代碼,只留下核心的邏輯

函數(shù)式接口:

任何接口,如果只包含<u>唯一一個抽象方法</u>,那么他就是一個函數(shù)式接口

對于函數(shù)式接口,我們可以通過lambda表達式來創(chuàng)建該接口的對象

() -> sout() ; 語句中,前面的( ) 就是new 的接口和其包含的抽象方法

注意:

接口的實現(xiàn)類如果放在主類里,要加static 關鍵字,即作為 內(nèi)部實現(xiàn)類

放在方法里,就是作為局部內(nèi)部類

作為匿名內(nèi)部類的話,new的是接口,而不是實現(xiàn)類

lambda表達式就是在匿名內(nèi)部類的基礎上省略了new 接口和重寫的抽象方法,只需要留下參入的參數(shù) -> ...

lambda表達式只能有一行代碼的情況下才能簡化為一行,如果有多行就必須用代碼塊包裹,即 -> 后加{ sout(); sout(); }; 前提是:必須是函數(shù)式接口

單個或多個參數(shù)也可以去掉參數(shù)類型,要去就都要去掉,多個參數(shù)就需要加括號包裹

多線程的優(yōu)勢和存儲的風險

多線程編程具備以下優(yōu)勢:

  • 提高系統(tǒng)的吞吐率(Throughout),多線程編程可以使一個進程有多個并發(fā)的操作

  • 提高響應性(Responsiveness),Web服務器會采用一些專門的線程負責用戶的請求處理,縮短用戶的等待時間

  • 充分利用多核處理器資源,通過多線程可以充分的利用CPU資源

多線程編程存在的風險:

  • 線程安全問題,多線程共享數(shù)據(jù)時,如果沒有采取正確的并發(fā)訪問控制措施,就可能會產(chǎn)生一些數(shù)據(jù)一致性問題,如讀取臟數(shù)據(jù)(過期的數(shù)據(jù)),如丟失數(shù)據(jù)更新。
  • 線程活性問題,由于程序自身的缺陷或者資源稀缺性導致線程一直處于非RUNNABLE狀態(tài),這就是線程活性問題,常見的活性故障有以下幾種:
    • 死鎖(DeadLock)
    • 鎖死(Lockout)
    • 活鎖(LiveLock)
    • 饑餓(Starvation)
  • 上下文切換(Context Switch),處理器從執(zhí)行一個線程切換到執(zhí)行另外一個線程
  • 可靠性,可能會由一個線程導致JVM意外終止,其他線程也無法執(zhí)行

線程狀態(tài)

創(chuàng)建狀態(tài)、就緒狀態(tài)、阻塞狀態(tài)、運行狀態(tài)、死亡狀態(tài)

<u>創(chuàng)建狀態(tài)</u>( new ) -> 調(diào)用start( ) 方法進入 <u>就緒狀態(tài)</u>,等待CPU的調(diào)度,調(diào)度完后 -> 進入<u>運行狀態(tài)</u>,運行狀態(tài)中調(diào)用sleep、wait方法等可以使線程進入<u>阻塞狀態(tài)</u> -> 阻塞狀態(tài)解除后使線程又進入就緒狀態(tài) -> 如果線程正常執(zhí)行完,就進入了<u>死亡狀態(tài)</u>

Thread.getState(); //獲取線程狀態(tài)

Thread.State

  • NEW :尚未啟動的線程處于此狀態(tài)
  • RUNNABLE :在Java虛擬機中執(zhí)行的線程處于此狀態(tài)
  • BLOCKED :被阻塞等待監(jiān)視器鎖定的線程處于此狀態(tài)
  • WAITING :正在等待另一個線程執(zhí)行待定動作的線程處于此狀態(tài)
  • TIMED_WAITING :正在等待另一個線程執(zhí)行動作達到指定等待時間的線程處于此狀態(tài)
  • TERMINATED :已退出的線程處于此狀態(tài)

一個線程可以在給定時間點處于一個狀態(tài),這些狀態(tài)不反映任何操作系統(tǒng)線程狀態(tài)的虛擬機狀態(tài)。

thread.getState() 方法可以獲取線程當前狀態(tài)

線程中斷或結束,一旦進入死亡狀態(tài),就不能再次啟動,線程只能啟動一次

線程方法

方法 說明
setPriority(int newPriority) 更改線程優(yōu)先級
static void sleep(long millis) 讓當前線程休眠指定毫秒數(shù)
void join( ) 等待該線程終止
static void yield( ) 禮讓線程 暫停當前正在執(zhí)行的線程對象,并執(zhí)行其他線程
void interrupt( ) 不建議使用 中斷線程,別用這個方式
boolean isAlive( ) 測試線程是否處于活躍狀態(tài)

不推薦jdk推薦的停止線程的方法(stop、destroy)

推薦線程自己停止下來,建議使用一個標志位進行終止變量,當flag=false,則終止線程運行

線程休眠:

  • sleep 指定當前線程阻塞的毫秒數(shù);
  • sleep 存在異常InterruptException;
  • sleep 時間達到后線程進入就緒狀態(tài);
  • sleep 可以模擬網(wǎng)絡延時,倒計時等;
  • 每一個對象都有一個鎖,sleep 不會釋放鎖;

線程禮讓 yield:

  • 禮讓線程,讓當前正在執(zhí)行的線程暫停,但不阻塞
  • 將線程從運行狀態(tài)轉為就緒狀態(tài)
  • 讓CPU重新調(diào)度,<u>禮讓不一定成功,取決于CPU</u>

合并線程 join:

  • Join 合并線程,待此線程執(zhí)行完成后,再執(zhí)行其他線程,其他線程阻塞(插隊)

線程優(yōu)先級

  • Java 提供一個線程調(diào)度器來監(jiān)控程序中啟動后進入就緒狀態(tài)的所有線程,線程調(diào)度器按照優(yōu)先級決定應該調(diào)度哪個線程來執(zhí)行
  • 線程的優(yōu)先級用數(shù)字表示,范圍從 1~10
    • Thread.MIN_PRIOROTY = 1 最小優(yōu)先級
    • Thread.MAX_PRIOROTY = 10 最大優(yōu)先級
    • Thread.NORM_PRIOROTY = 5 默認優(yōu)先級(main)

注意:線程優(yōu)先級大不一定先執(zhí)行,真是權重更大了而已,獲得的資源更多,還是看CPU的調(diào)度

  • 使用以下方式該百年或獲取優(yōu)先級
    • getPriority( )
    • setPriority( int xxx )

守護線程

  • 線程分為用戶線程和守護線程
  • 虛擬機必須確保用戶線程執(zhí)行完畢
  • 虛擬機不用等待守護線程執(zhí)行完畢

如:后臺記錄操作日志、監(jiān)控內(nèi)存、垃圾回收等待機制..

//設置守護線程
thread.setDaemon(true);     //默認是false表示是用戶線程,正常線程都是用戶線程

中斷線程

只是給線程打上一個中斷的標志,但是線程并不會中斷,需要判斷過后起標志再去操作。

Thread.interrupt(); //中斷

t.isInterrupted(); //判斷是否中斷(是否打上標志)


線程安全問題

    非線程安全主要是指多個線程對同一個對象的實例變量進行操作時,會出現(xiàn)值被更改,值不同步的情況。

    線程安全問題表現(xiàn)為三個方面:原子性、可見性和有序性。
  • 原子性

    原子(Atomic)就是不可分割的意思,原子操作的不可分割有兩層含義:

    • 訪問(讀/寫)某個共享變量的操作從其他線程來看,該操作要么已經(jīng)執(zhí)行完畢,要么尚未發(fā)生,即其他線程看不到當前操作的中間結果
    • 訪問同一組共享變量的原子操作是不能交錯的

    Java有兩種方式實現(xiàn)原子性:1)鎖;2)處理器的CAS指令

    • 鎖具有排他性,保證共享變量在某一時刻只能被一個線程訪問。
    • CAS指令直接在硬件(處理器和內(nèi)存)層次上實現(xiàn),看作硬件鎖
  • 可見性

    在多線程環(huán)境中,一個線程對某個共享變量進行更新后,后續(xù)的其他線程可能無法立即讀到這個更新的結果

  • 有序性

    有序性(Ordering)是指在什么情況下一個處理器上運行的一個線程所執(zhí)行的內(nèi)存訪問操作在另外一個處理器運行的其他線程看來是亂序的。

    亂序是指內(nèi)存訪問操作的順序看起來發(fā)生了變化

    在多核處理器的環(huán)境下,編寫的順序結構,這種操作執(zhí)行的順序可能是沒有保障的:

    • 編譯器可能會改變兩個操作的先后順序;
    • 處理器也可能不會按照目標代碼的順序執(zhí)行;
    • 這種一個處理器上執(zhí)行的多個操作,在其他處理器來看它的順序與目標代碼指定的順序可能不一樣,這種現(xiàn)象就叫重排序
      • 重排序是對內(nèi)存訪問有序操作的一種優(yōu)化,可以不影響單線程程序正確的情況下提升程序的性能,但是可能對多線程程序的正確性產(chǎn)生影響,即可能導致線程安全問題

    可以把重排序分為指令重排序和存儲子系統(tǒng)重排序兩種:

    • 指令重排序主要是由JIT編譯器,處理器引起的,指程序順序和執(zhí)行順序不一樣
    • 存儲子系統(tǒng)重排序是由高速緩存,寫緩沖器引起的,感知順序與執(zhí)行順序不一致

指令重排序

    在源碼順序與程序順序不一致或者程序順序與執(zhí)行順序不一致的情況下,我們就說發(fā)生了指令重排序(Instruction Reorder)

    指令重排是一種動作,確實對指令的順序做出了調(diào)整,重排序的對象指令

    javac編譯器一般不會執(zhí)行指令重排序,而 JIT編譯器可能執(zhí)行指令重排序,處理器也可能執(zhí)行指令重排序,使得執(zhí)行順序和程序順序不一致。

    指令重排不會對單線程程序的結果正確性產(chǎn)生影響,可能導致對多線程程序出現(xiàn)非預期結果。

存儲子系統(tǒng)重排序

    存儲子系統(tǒng)是指寫緩沖器與高速緩存。

    高速緩存(Cache)是CPU中為了匹配與主內(nèi)存處理速度不匹配而設計的一個高速緩存。

    寫緩沖器(Store buffer,Write buffer)用來提高寫高速緩存操作的效率

    即使處理器嚴格按照程序順序執(zhí)行兩個內(nèi)存訪問操作,在存儲子系統(tǒng)的作用下,其他處理器對這兩個操作的感知順序與程序順序不一致,即這兩個操作的執(zhí)行順序看起來像是發(fā)生了變化,這種現(xiàn)象稱為 存儲子系統(tǒng)重排序。

    存儲子系統(tǒng)重排序并沒有真正的對指令執(zhí)行順序進行調(diào)整,而是造成一種指令執(zhí)行順序被調(diào)整的假象。

    存儲子系統(tǒng)重排序對象是內(nèi)存操作的結果。

保證內(nèi)存訪問的順序性

    實質(zhì)上就是怎么解決重排序導致的線程安全問題。

    可以使用volatile關鍵字,synchronized關鍵字實現(xiàn)有序性。

線程同步

    線程同步機制是用于協(xié)調(diào)線程之間的數(shù)據(jù)訪問的機制,該機制可以保障線程安全。

    Java平臺提供的線程同步機制包括:鎖Lock,volatile關鍵字,final關鍵字,static關鍵字,以及相關的API,如Object.wait()/Object.notify()等

<u>同一個對象被多個線程同時操作,</u>這時候就需要線程同步,線程同步其實就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面的線程使用完畢,下一個線程再使用

鎖的概述

    鎖具有排他性(Exclusive),即一個鎖一次只能被一個線程持有,這種鎖稱為排他鎖或互斥鎖(Mutex)

    **JVM把鎖分為內(nèi)部鎖和顯式鎖,內(nèi)部鎖通過synchronized關鍵字實現(xiàn);顯式鎖通過java.concurrent.locks.lock接口的實現(xiàn)類實現(xiàn)的。**

鎖的作用

    鎖可以實現(xiàn)對共享數(shù)據(jù)的安全訪問,保障線程的原子性,可見性與有序性。鎖是通過互斥保障原子性,一個鎖只能被一個線程持有,這就保證臨界區(qū)的代碼一次只能被一個線程執(zhí)行。使得臨界區(qū)代碼所執(zhí)行的操作自然而然的具有不可分割的特性(原子性)

    可見性的保障是用過寫線程沖刷處理器的緩存和讀線程刷新處理器緩存這兩個動作實現(xiàn)的。在java平臺中,鎖的獲得隱含著刷新處理器緩存的動作,而鎖的釋放隱含著沖刷處理器緩存的動作。

    鎖能夠保障有序性,寫線程在臨界區(qū)所執(zhí)行的操作,在讀線程所執(zhí)行的臨界區(qū)看來像是完全按照源碼順序執(zhí)行的。

注意:

    使用鎖保障線程的安全性,必須滿足以下條件:
  • 這些線程在訪問共享數(shù)據(jù)時必須使用同一個鎖
  • 即使是讀取共享數(shù)據(jù)的線程也需要使用同步鎖

鎖相關概念

  • 可重入性(Reentrancy)

    一個線程持有該鎖的時候能再次(多次)申請該鎖。即如果一個線程持有的一個鎖的時候還能夠繼續(xù)成功申請該鎖,稱該鎖是可重入的,否則就稱該鎖不可重入的。

  • 鎖的爭用與調(diào)度

    Java平臺中內(nèi)部鎖屬于非公平鎖,顯式鎖lock既支持公平鎖又支持非公平鎖

  • 鎖的粒度

    一個鎖可以保護的共享數(shù)據(jù)的數(shù)量大小稱為鎖的粒度。

    鎖保護的共享數(shù)據(jù)量大,稱該鎖的粒度粗,否則就稱該鎖粒度細

    鎖的粒度過粗會導致線程在申請鎖時會進行不必要的等待,鎖的粒度過細會增加鎖調(diào)度的開銷。

內(nèi)部鎖:synchronized關鍵字

    Java中的每個對象都有一個與之關聯(lián)的內(nèi)部鎖,這種鎖也稱為監(jiān)視器(Monitor),這種鎖是一種排他鎖,可以保障原子性、可見性和有序性

    內(nèi)部鎖是通過synchronized關鍵字實現(xiàn)的,synchronized關鍵字可以修飾代碼塊,修飾該方法。

線程同步實現(xiàn)條件:隊列+鎖

自我理解:公共廁所的例子!線程在線程池排隊訪問對象,形成隊列,<u>每個對象都有一個鎖</u>一個線程訪問對象后獲得對象的排他鎖,獨占資源,使其他線程必須等待,使用完后釋放鎖即可

存在的問題:

  • 一個線程持有鎖會導致其他所有需要此鎖的線程掛起
  • 在多線程的競爭下,加鎖會導致比較多的上下文切換 和 調(diào)度延時,引起性能問題
  • 如果一個優(yōu)先級高的線程等待一個優(yōu)先級低的線程釋放鎖,引起優(yōu)先級倒置,引起性能問題

同步方法:

synchronized 方法

public synchronized void method(int args){}     //同步方法
  • synchronized 方法控制對“對象”的訪問,每個對象應有一把鎖,每個synchronized方法都必須獲得調(diào)用該方法的對象的鎖才能執(zhí)行,否則線程會阻塞,方法一旦執(zhí)行,就獨占該鎖,直到該方法返回才釋放鎖,后面被阻塞的線程才能獲得這個鎖,繼續(xù)執(zhí)行

缺陷:若將一個大的方法申明為synchronized,將會影響效率

注意:

  • 方法里面需要修改內(nèi)容才需要鎖,鎖的太多浪費資源,只讀內(nèi)容不需要加鎖

同步塊:

synchronized (Obj) {//同步代碼塊,Obj:被鎖的對象,方法丟在塊里
    同步代碼塊,訪問共享數(shù)據(jù)
}   
  • Obj 稱之為 同步監(jiān)視器
    • Obj可以是任何對象,但是推薦使用共享資源作為同步監(jiān)視器
    • 同步方法中無需指定同步監(jiān)視器,因為同步方法的同步監(jiān)視器就是this,就是這個對象本身,或者是class
  • 同步監(jiān)視器的執(zhí)行過程
    1. 第一個線程訪問,鎖定同步監(jiān)視器,執(zhí)行其中代碼
    2. 第二個線程訪問,發(fā)現(xiàn)同步監(jiān)視器被鎖定,無法訪問
    3. 第一個線程訪問完畢,解鎖同步監(jiān)視器
    4. 第二個線程訪問,發(fā)現(xiàn)沒鎖,然后鎖定訪問

注意:如果需要鎖的對象就是this,那就可以直接使用同步方法,就是在方法前加synchronized,如果需要加鎖的對象不是this,而是其他對象,就寫同步塊,鎖定指定的對象,然后將方法寫在塊中。(哪個對象的屬性進行增刪改等修改了,就是需要鎖的對象)

    同步方法鎖的粒度粗,并發(fā)效率低

    同步代碼塊鎖的粒度細,并發(fā)效率高

臟讀

出現(xiàn)讀取屬性值出現(xiàn)一些意外,讀取的是中間值,而不是修改之后的值。

出現(xiàn)臟讀的原因是:對共享數(shù)據(jù)的修改 與對共享數(shù)據(jù)的讀取不同步

解決方法:對修改數(shù)據(jù)的代碼塊進行同步,還要對讀取數(shù)據(jù)的代碼塊進行同步

線程出現(xiàn)異常會自動釋放鎖


死鎖:

多個線程各自占用一些共享資源,并且互相等待其他線程占有的資源才能運行,而導致兩個或多個線程都在等待對方釋放資源,都停止執(zhí)行的情形,某一個同步塊同時擁有“兩個以上對象的鎖”時,就可能發(fā)生<u>死鎖</u>問題

  • 產(chǎn)生死鎖的四個必要條件:
    • 互斥條件:一個資源每次只能被一個進程使用
    • 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得資源保持不放
    • 不剝奪條件:進程已獲得的資源,在未使用完之前,不能強行剝奪
    • 循環(huán)等待條件:若干進程之間形成形成一種頭尾相接的循環(huán)等待資源關系

只需要破壞以上四個必要條件中的任意一個或多個,就能避免死鎖發(fā)生。


輕量級同步機制:volatile關鍵字

volatile的作用

    volatile關鍵的作用使變量在多個線程之間可見。可以強制線程從公共內(nèi)存中讀取變量的值,而不是從工作內(nèi)存中讀取。

volatile與synchronized比較

  • volatile關鍵字是線程同步的輕量級實現(xiàn),所以volatile性能肯定比synchronized要好;volatile只能修飾變量,而synchronized還可以修飾方法,代碼塊。
  • 多線程訪問volatile變量不會發(fā)生阻塞,而synchronized可能會阻塞
  • volatile能保證數(shù)據(jù)的可見性,但是不能保證原子性;而synchronized可以保證原子性,也可以保證可見性
  • 關鍵字volatile解決的是變量在多個線程之間的可見性;synchronized關鍵字解決多個線程之間訪問公共資源的同步性。

volatile非原子性

    volatile關鍵字增加了實例變量在多個線程之間的可見性,但是它不具備原子性

常用的原子類進行自增自減操作

    i++操作不是原子操作,所以不能保證線程安全,除了使用synchronized進行同步外,也可以使用AtomicInteger/AtomicLong原子類進行實現(xiàn)。

CAS(Compare And Swap)

    CAS是由硬件實現(xiàn)的。

    CAS可以將read- modify - write這類的操作轉換為原子操作

CAS原理:

    在把數(shù)據(jù)更新到主內(nèi)存時,再次讀取主內(nèi)存變量的值,如果現(xiàn)在變量的值與期望的值(操作起始時讀取的值)一樣就更新。

    CAS實現(xiàn)原子操作背后有一個假設:共享變量的當前值與當前線程提供的期望值相同,就認為這個變量沒有被其他線程修改過。

    但是,實際上這種假設不一定總是成立。CAS會有ABA問題發(fā)生

    如果想要規(guī)避ABA問題,可以為共享變量引入一個修訂號(時間戳),每次修改共享變量時,相應的修訂號就會增加1,每次對共享變量的修改都會導致修訂號的增加,通過修訂號依然可以準確判斷是否被其他線程修改過。AtomicStampedReference類就是基于這種思想產(chǎn)生的。

原子變量類

    原子變量類是基于CAS實現(xiàn)的,當對共享變量進行read- modify - write更新操作時,通過原子變量類可以保障操作的原子性和可見性。對變量的read- modify - write更新操作是指當前操作不是一個簡單的賦值,而是變量的新值依賴變量的舊值。由于volatile只能保障變量的可見性,無法保障原子性,原子變量類內(nèi)部就是借助一個volatile變量,并且保障了該變量的read- modify - write操作的原子性,有時把原子變量類看作增強的volatile變量,原子變量類有12個:
分組 原子變量類
基礎數(shù)據(jù)型 AtomicInteger,AtomicLong,AtomicBoolean
數(shù)組型 AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
字段更新器 AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
引用型 AtomicReference,AtomicStampedReference,AtomicMarkableReference

ReentrantLock鎖

  • 從JDK5.0開始,Java提供了強大的線程同步機制--通過顯示定義同步鎖對象老師先同步。同步鎖使用Lock對象充當
  • 鎖提供了對共享資源的獨占訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象
  • ReentrantLock 類實現(xiàn)了Lock,它擁有與synchronized 相同的并發(fā)性和內(nèi)存語義,在實現(xiàn)線程安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。(ReentrantLock 可重入鎖,它的功能比synchronized多)

鎖的可重入性

鎖的可重入性是指,當一個線程獲得一個對象鎖后,再次請求該對象鎖時是可以獲得該對象鎖的。

//定義lock鎖
ReentrantLock lock = new ReentrantLock();
lock.lock()     //加鎖
lock.unlock()       //解鎖

lockInterruptibly()

lockInterruptibly()方法的作用:如果當前線程未被中斷則獲得鎖,如果當前線程被中斷則出現(xiàn)異常。<u>可以解決死鎖問題</u>

lock.lock();                //獲得鎖定,即使調(diào)用了線程的interrupt()方法,線程也不會真正中斷
lock.lockInterruptibly();           //如果線程中斷了,不會獲得鎖,會發(fā)生異常

tryLock()方法

tryLock(long time,TimeUnit unit)的作用在給定等待時長內(nèi)鎖沒有被另外的線程持有,并且當前線程也沒有中斷,則獲得該鎖,通過該方法可以實現(xiàn)鎖對象的限時等待。

tryLock()無參方法僅在調(diào)用時鎖定未被其他線程持有的鎖,如果調(diào)用方法時,鎖對象被其他線程持有,則放棄。

<u>使用tryLock()可以避免死鎖問題</u>

newCondition()方法

關鍵字synchronized與wait()/notify()這兩個方法一起使用可以實現(xiàn)等待/通知模式,Lock鎖的newCondition()方法返回Condition對象,Condition類也可以實現(xiàn)等待/通知模式。

使用notify()通知時,JVM會隨機喚醒某個等待的線程,使用Condition類則可以<u>進行選擇性通知</u>。

Condition比較常用的兩個方法:

  • await()會使當前線程等待,同時會釋放鎖。 當其他線程調(diào)用signal()時,線程會重新獲得鎖并繼續(xù)執(zhí)行。
  • signal()用于喚醒一個等待線程。

注意:

在調(diào)用Condition的await()/signal()方法前,也需要線程持有相關的Lock鎖。調(diào)用await()方法后線程會釋放這個鎖,調(diào)用signal()方法后會從當前的Condition對象的等待隊列中,喚醒一個線程,喚醒的線程嘗試獲得鎖,一旦獲得鎖成功后就繼續(xù)執(zhí)行。

公平鎖和非公平鎖

大多數(shù)情況下,鎖的申請都是非公平的,系統(tǒng)只會從阻塞隊列中隨機選擇一個線程,無法保證公平性。

公平的鎖會按照時間的先后順序,保證先到先得,公平鎖這一特點不會出現(xiàn)線程饑餓的現(xiàn)象。多個線程不會發(fā)生同一個線程連續(xù)多次獲得鎖的可能,保證鎖的公平性。公平鎖看起來公平,但是要實現(xiàn)公平鎖必須要求系統(tǒng)維護一個有序隊列,所以公平鎖的實現(xiàn)成本較高,性能也較低,因此默認情況下鎖是非公平的。

  • synchronized內(nèi)部鎖就是非公平的,ReentrantLock重入鎖提供了一個構造方法:ReentrantLock(boolean fair),當在創(chuàng)建鎖對象時實參傳遞true就可以把該鎖設置為公平鎖

ReentrantLock常用方法

  • int getHoldCount():返回當前線程調(diào)用lock()方法的次數(shù)
  • int getQueueLength():返回正等待獲得鎖的線程預估數(shù)
  • int getWaitQueueLength(Condition condition):返回與Condition條件相關的等待的線程的預估數(shù)
  • boolean hasQueuedThread(Thread thread):查詢參數(shù)指定的線程是否在等待獲得鎖
  • boolean hasQueuedThreads():查詢是否還有線程在等待獲得該鎖
  • boolean hasWaiters(Condition condition):查詢是否還有線程正在等待指定的Condition條件
  • boolean isFair():判斷是否為公平鎖
  • boolean isHeldByCurrentThread():判斷當前線程是否持有該鎖

synchronized與Lock的對比

  • Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖),synchronized是隱式鎖,出了作用域自動釋放
  • Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
  • 使用Lock鎖,JVM將花費較少的時間來調(diào)度線程,性能更好。并且具有更好的擴展性(提供更多的子類)
  • 使用優(yōu)先順序
    • Lock > 同步代碼塊(已經(jīng)進入了方法體,分配了相應資源)> 同步方法(方法體外)

對于synchronized內(nèi)部鎖來說,如果一個線程在等待鎖,只有兩種結果:要么該線程獲得鎖繼續(xù)執(zhí)行,要么就保持等待

對于ReentrantLock可重入鎖來說,提供另外一種可能,在等待鎖的過程中,程序可以根據(jù)需要取消對鎖的請求。

ReentrantReadWriteLock讀寫鎖

synchronized內(nèi)部鎖和ReentrantLock鎖都是獨占鎖(排他鎖),同一時間只允許一個線程執(zhí)行同步代碼塊,可以保證線程的安全性,但是執(zhí)行效率低。

ReentrantReadWriteLock讀寫鎖是一種改進的排他鎖,也可以稱作共享/排他鎖。允許多個線程同時讀取共享數(shù)據(jù),但是一次只允許一個線程對共享數(shù)據(jù)進行更新。

讀寫鎖通過讀鎖和寫鎖來完成讀寫操作。線程在讀取共享數(shù)據(jù)前必須先持有讀鎖,該讀鎖可以同時被多個線程持有,即它是共享的。寫鎖是排他的,線程在更新共享數(shù)據(jù)前必須先持有寫鎖, 一個線程持有寫鎖時其他線程線程無法獲得相應的鎖。

讀鎖只是在讀線程之間共享,任何一個線程持有讀鎖時,其他線程都無法持有寫鎖,保證線程在讀取數(shù)據(jù)期間沒有其他線程對數(shù)據(jù)進行更新,使得讀線程能夠讀取數(shù)據(jù)的最新值,保證讀數(shù)據(jù)期間共享變量不被修改

//定義讀寫鎖
ReadWriteLock rwlock = new ReentrantReadWriteLock();
//獲得讀鎖
Lock readLock = rwlock.readLock();
//獲得寫鎖
Lock writeLock = rwlock.writeLock();
//讀線程方法
readLock.lock();
try{
        讀取數(shù)據(jù);
}finally{
        readLock.unlock();
}
//寫線程方法
writeLock.lock();
try{
        更新數(shù)據(jù);
}finally{
        writeLock.unlock(); 
}

等待/通知機制

Object類中的wait()方法可以使執(zhí)行當前代碼的線程等待,暫停執(zhí)行,直到接到通知或被中斷為止。(會釋放鎖)

注意:

  1. wait()方法只能在同步代碼塊中由鎖對象調(diào)用

  2. 調(diào)用wait()方法,當前線程會釋放鎖

    Object類中的notify()可以喚醒線程,該方法也必須在同步代碼塊中由鎖對象調(diào)用,沒有使用鎖對象調(diào)用wait()/notify()方法會拋出llegalMonitorStateException異常,如果有多個等待的線程,notify()方法只能喚醒其中一個。在同步代碼塊中調(diào)用notify()方法后,并不會立即釋放鎖對象,需要等當前同步代碼塊執(zhí)行完后才會釋放鎖對象,一般將notify()放在同步代碼塊的最后。

interrupt()方法會中斷wait()等待

interrupt方法會中斷wait方法,會釋放鎖。

wait(long)的使用

wait(long)帶有l(wèi)ong類型參數(shù)的wait()等待,如果在參數(shù)指定的時間內(nèi)沒有被喚醒,超時后會自動喚醒

通知過早問題

線程wait()等待后,可以調(diào)用notify()喚醒線程,如果notify()喚醒的過早,在等待之前就調(diào)用了,notify()可能會打亂程序正常的運行邏輯(就是喚醒線程在等待線程之前先執(zhí)行了,導致等待線程無法被喚醒,可以定義一個靜態(tài)變量static boolean flag = true;作為線程運行的標志,在等待線程里加一個條件while(flag)判斷線程狀態(tài),在喚醒線程中,將flag改為false,這樣如果先執(zhí)行喚醒線程,那等待線程也不會執(zhí)行)

wait等待條件發(fā)生了變化

在使用wait/notify模式時,如果wait條件發(fā)生了變化,也可能會造成邏輯的混亂

線程協(xié)作

  • 在生產(chǎn)者消費者問題中,僅有synchronized是不夠的
    • synchronized 可阻止并發(fā)更新同一個共享資源,實現(xiàn)了同步
    • synchronized 不能用來實現(xiàn)不同線程之間的消息傳遞(通信)
  • java提供了幾個方法解決線程之間的通信問題
方法名 作用
wait( ) 表示線程會一直等待,直到其他線程通知,與sleep不同,會釋放鎖
wait(long timeout) 指定等待的毫秒數(shù)
notify( ) 喚醒一個處于扽等該狀態(tài)的線程
notifyAll( ) 喚醒同一個對象上所有調(diào)用wait( )方法的線程,優(yōu)先級別高的線程優(yōu)先調(diào)度

注意:

均是Object類的方法,都只能在同步方法或者同步代碼塊中使用,否則會拋出異常IIIegaMonitorStateException

解決方式1

并發(fā)協(xié)作模式“生產(chǎn)者/消費者”--> <u>管程法</u>

  • 生產(chǎn)者:負責生成數(shù)據(jù)的模塊
  • 消費者:負責處理數(shù)據(jù)的模塊
  • 緩沖區(qū):消費者不能直接使用生產(chǎn)者的數(shù)據(jù),他們之間有個“緩沖區(qū)”

生產(chǎn)者將生產(chǎn)好的數(shù)據(jù)放入緩沖區(qū)中,消費者從緩沖區(qū)拿出數(shù)據(jù)

解決方式2

并發(fā)協(xié)作模式“生產(chǎn)者/消費者模式”--> 信號燈法

生產(chǎn)者消費者模式

在java中,負責產(chǎn)生數(shù)據(jù)的模塊是生產(chǎn)者,負責使用數(shù)據(jù)的模塊是消費者,生產(chǎn)者消費者解決數(shù)據(jù)的平衡問題,即先有數(shù)據(jù)然后才能使用,沒有數(shù)據(jù)時消費者需要等待。

  1. 生產(chǎn)-消費:操作數(shù)據(jù)
  2. 多生產(chǎn)-多消費:notify()不能保證是生產(chǎn)者喚醒消費者,如果生產(chǎn)者喚醒的還是生產(chǎn)者可能會出現(xiàn)假死的情況(所以喚醒操作要用notifyAll)
  3. 操作棧

通過管道實現(xiàn)線程間的通信

在java.io包中的PipeStream管道流用于在線程之間傳送數(shù)據(jù),一個線程發(fā)送數(shù)據(jù)到輸出管道,另外一個線程從輸入管道中讀取數(shù)據(jù)。相關的類包括:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter字符流。

ThreadLocal的使用

除了控制資源的訪問外,還可以通過增加資源來保證線程安全。ThreadLocal主要解決為每個線程綁定自己的值

ThreadLocal提供了線程內(nèi)存儲變量的能力,這些變量不同之處在于每一個線程讀取的變量是對應的互相獨立的。通過get和set方法就可以得到當前線程對應的值。

線程管理

線程組

Thread類有幾個構造方法允許在創(chuàng)建線程時指定線程組,如果在創(chuàng)建線程時沒有制定線程組,則該線程屬于父線程所在的線程組。JVM在創(chuàng)建main線程時會為他指定一個線程組,因此每個Java線程都有一個線程組與之關聯(lián),可以調(diào)用線程的getThreadGroup()方法返回線程組。

捕獲線程的執(zhí)行異常

在線程的run方法中,如果有受檢異常必須進行捕獲處理,如果想要獲得run()方法中出現(xiàn)的運行時異常信息,可以通過回調(diào)UncaughtExceptionhandler接口獲得哪個線程出現(xiàn)了運行時異常。在Thread類中有關處理運行時異常的方法有:

  • getDefaultUncaughtExceptionhandler():獲得全局的(默認的)UncaughtExceptionhandler
  • getUncaughtExceptionhandler():獲得當前線程的UncaughtExceptionhandler
  • set

設置線程異?;卣{(diào)接口

注入Hook勾子線程

很多軟件包括mysql、zookeeper、Kafka等都存在Hook線程的校驗機制,目的是校驗進程是否已啟動,防止重復啟動程序。

當JVM退出時會執(zhí)行Hook線程,經(jīng)常在程序啟動時創(chuàng)建一個.lock文件,用.lock文件校驗程序是否啟動,在程序退出(JVM退出)時刪除該.lock文件,在Hook線程中除了防止重新啟動進程外,還可以做資源釋放,盡量避免在Hook線程中進行復雜的操作。

線程池

  • 思路:提前創(chuàng)建好多個線程,放入線程池中,使用時直接獲取,使用完放回池中,可以避免頻繁創(chuàng)建銷毀,實現(xiàn)重復利用。

  • 好處:

    • 提供響應速度(減少了創(chuàng)建新線程的時間)
    • 降低資源消耗(重復利用線程池中線程,不需要每次都創(chuàng)建)
    • 便于線程管理
      • corePoolSize:核心池的大小
      • maximumPoolsize:最大線程數(shù)
      • keepAliveTime:線程沒有任務時最多保持多長時間后會終止
//創(chuàng)建服務,創(chuàng)建線程池
ExecutorService service = Executor.newFixedThreadPool(x); //x:線程數(shù)
//通過線程池執(zhí)行線程也可以,通過start也可以
service.execute(new MyThread());
//關閉鏈接
service.shutdown();
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

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