Java日常學(xué)習(xí)總結(jié)并發(fā)進(jìn)階之鎖與同步篇

Java日常學(xué)習(xí)總結(jié)并發(fā)進(jìn)階之鎖與同步篇

創(chuàng)建一個(gè)新線程的三種方法

通過(guò)Runnable接口創(chuàng)建線程類(lèi)

  1. 定義runnable接口的實(shí)現(xiàn)類(lèi),并重寫(xiě)該接口的run()方法,該run()方法的方法體同樣是該線程的線程執(zhí)行體。
  2. 創(chuàng)建 Runnable實(shí)現(xiàn)類(lèi)的實(shí)例,并依此實(shí)例作為T(mén)hread的target來(lái)創(chuàng)建Thread對(duì)象,該Thread對(duì)象才是真正的線程對(duì)象。
  3. 調(diào)用線程對(duì)象的start()方法來(lái)啟動(dòng)該線程。
public class RunnableThreadTest implements Runnable {
    private int i;
    public void run() {
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+ " " + i);
        }
    }
    public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
        System.out.println(Thread.currentThread().getName()+ " " + i);
        if (i == 20) {
            RunnableThreadTest rtt = new RunnableThreadTest();
            new Thread(rtt, "新線程1").start();
            new Thread(rtt, "新線程2").start();
        }
    }
    }
}

繼承Thread類(lèi)創(chuàng)建線程類(lèi)

  1. 定義Thread類(lèi)的子類(lèi),并重寫(xiě)該類(lèi)的run方法,該run方法的方法體就代表了線程要完成的任務(wù)。因此把run()方法稱(chēng)為執(zhí)行體。
  2. 創(chuàng)建Thread子類(lèi)的實(shí)例,即創(chuàng)建了線程對(duì)象。
  3. 調(diào)用線程對(duì)象的start()方法來(lái)啟動(dòng)該線程。
public class FirstThreadTest extends Thread {
    int i = 0;
    //重寫(xiě)run方法,run方法的方法體就是現(xiàn)場(chǎng)執(zhí)行體
    public void run() {
        for (; i < 100; i++) {
            System.out.println(getName() + " " + i);
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+ " : " + i);
            if (i == 20) {
                new FirstThreadTest().start();
                new FirstThreadTest().start();
            }
        }
    }
}

中斷線程

沒(méi)有任何語(yǔ)言方面的需求要求一個(gè)被中斷的線程應(yīng)該終止。中斷一個(gè)線程不過(guò)是引起它的注意。被中斷的線程可以決定如何響應(yīng)中斷。某些線程是如此重要以至于應(yīng)該處理完異常后, 繼續(xù)執(zhí)行, 而不理會(huì)中斷。但是,更普遍的情況是,線程將簡(jiǎn)單地將中斷作為一個(gè)終止的請(qǐng)求。

線程狀態(tài)

新創(chuàng)建

剛new出來(lái)的Thread還沒(méi)有被運(yùn)行

可運(yùn)行

一旦調(diào)用start 方法,線程處于runnable狀態(tài)。一個(gè)可運(yùn)行的線桿可能正在運(yùn)行也可能沒(méi)有運(yùn)行, 這取決于操作系統(tǒng)給線程提供運(yùn)行的時(shí)間。

被阻塞線程和等待線程

當(dāng)線程處于被阻塞或等待狀態(tài)時(shí), 它暫時(shí)不活動(dòng)。它不運(yùn)行任何代碼且消耗最少的資源。直到線程調(diào)度器重新激活它。細(xì)節(jié)取決于它是怎樣達(dá)到非活動(dòng)狀態(tài)的

  • 當(dāng)一個(gè)線程試圖獲取一個(gè)內(nèi)部的對(duì)象鎖(而不是javiutiUoncurrent 庫(kù)中的鎖),而該鎖被其他線程持有, 則該線程進(jìn)人阻塞狀態(tài)。當(dāng)所有其他線程釋放該鎖,并且線程調(diào)度器允許本線程持有它的時(shí)候,該線程將變成非阻塞狀態(tài)。
  • 當(dāng)線程等待另一個(gè)線程通知調(diào)度器一個(gè)條件時(shí),它自己進(jìn)入等待狀態(tài)。在調(diào)用Object.wait方法或Thread.join方法,或者是等待java,util.concurrent 庫(kù)中的Lock 或Condition 時(shí),就會(huì)出現(xiàn)這種情況。實(shí)際上,被阻塞狀態(tài)與等待狀態(tài)是有很大不同的。
  • 有幾個(gè)方法有一個(gè)超時(shí)參數(shù)。調(diào)用它們導(dǎo)致線程進(jìn)人計(jì)時(shí)等待(timed waiting) 狀態(tài)。這一狀態(tài)將一直保持到超時(shí)期滿(mǎn)或者接收到適當(dāng)?shù)耐ㄖ?。帶有超時(shí)參數(shù)的方法有Thread.sleep 和Object.wait、Thread.join、Lock,tryLock以及Condition.await的計(jì)時(shí)版。

被終止的線程

線程因如下兩個(gè)原因之一而被終止:

  • 因?yàn)閞un方法正常退出而自然死亡。
  • 因?yàn)橐粋€(gè)沒(méi)有捕獲的異常終止了run方法而意外死亡。

線程屬性

線程優(yōu)先級(jí)

每當(dāng)線程調(diào)度器有機(jī)會(huì)選擇新線程時(shí),它首先選擇具有較高優(yōu)先級(jí)的線程。但是,線程優(yōu)先級(jí)是高度依賴(lài)于系統(tǒng)的。當(dāng)虛擬機(jī)依賴(lài)于宿主機(jī)平臺(tái)的線程實(shí)現(xiàn)機(jī)制時(shí),Java 線程的優(yōu)先級(jí)被映射到宿主機(jī)平臺(tái)的優(yōu)先級(jí)上,優(yōu)先級(jí)個(gè)數(shù)也許更多,也許更少。

static void yield( )
導(dǎo)致當(dāng)前執(zhí)行線程處于讓步狀態(tài)。如果有其他的可運(yùn)行線程具有至少與此線程同樣高的優(yōu)先級(jí),那么這些線程接下來(lái)會(huì)被調(diào)度。注意,這是一個(gè)靜態(tài)方法。

守護(hù)線程

同步

為了避免多線程引起的對(duì)共享數(shù)據(jù)的訛誤,必須學(xué)習(xí)如何同步存取。

競(jìng)爭(zhēng)條件詳解

當(dāng)兩個(gè)線程試圖同時(shí)更新同一個(gè)賬戶(hù)的時(shí)候,這個(gè)問(wèn)題就出現(xiàn)了。假定兩個(gè)線程同時(shí)執(zhí)行指令accounts[to] += amount;問(wèn)題在于這不是原子操作。該指令可能被處理如下:

  1. accounts[to]加載到寄存器。
  2. 增加amount 。
  3. 將結(jié)果寫(xiě)回accounts[to]。

現(xiàn)在,假定第1個(gè)線程執(zhí)行步驟1和2, 然后,它被剝奪了運(yùn)行權(quán)。假定第2個(gè)線程被喚醒并修改了accounts 數(shù)組中的同一項(xiàng)。然后,第1個(gè)線程被喚醒并完成其第3步。
這樣,這一動(dòng)作擦去了第二個(gè)線程所做的更新。于是,總金額不再正確。
因此加入同步鎖以避免在該線程沒(méi)有完成操作之前,被其他線程的調(diào)用,從而保證了該變量的唯一性和準(zhǔn)確性。

鎖同步

鎖Lock

有兩種機(jī)制防止代碼塊受并發(fā)訪問(wèn)的干擾。Java語(yǔ)言提供一個(gè)synchronized關(guān)鍵字達(dá)到這一目的,并且Java SE 5.0引入了ReentrantLock 類(lèi)。

public class Bank{
    private Lock bankLock = new ReentrantLock0 ;// ReentrantLock implements the Lock interface
    public void transfer(int from, intto, int amount){
        bankLock.lock();
        try
        {
            System.out.print(Thread.currentThread0);
            accounts[from] -= amount;
            System.out.printf(" %10.2f from %A to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n",getTotalBalance());
        }
        finally
        {
            banklock.unlockO;
        }
    }
}

重入Lock是一個(gè)更強(qiáng)大的工具,他有一個(gè)重入功能————當(dāng)一個(gè)線程得到一個(gè)對(duì)象后,再次請(qǐng)求該對(duì)象鎖時(shí)是可以再次得到該對(duì)象的鎖的。
具體概念就是:自己可以再次獲取自己的內(nèi)部鎖。因?yàn)榫€程可以重復(fù)地獲得已經(jīng)持有的鎖。鎖保持一個(gè)持有計(jì)數(shù)(holdcount ) 來(lái)跟蹤對(duì)lock 方法的嵌套調(diào)用。線程在每一次調(diào)用lock 都要調(diào)用unlock 來(lái)釋放鎖。由于這一特性,被一個(gè)鎖保護(hù)的代碼可以調(diào)用另一個(gè)使用相同的鎖的方法。
假定一個(gè)線程調(diào)用transfer, 在執(zhí)行結(jié)束前被剝奪了運(yùn)行權(quán)。假定第二個(gè)線程也調(diào)用transfer, 由于第二個(gè)線程不能獲得鎖,將在調(diào)用lock 方法時(shí)被阻塞。它必須等待第一個(gè)線程完成transfer 方法的執(zhí)行之后才能再度被激活。當(dāng)?shù)谝粋€(gè)線程釋放鎖時(shí),那么第二個(gè)線程才能開(kāi)始運(yùn)行

公平鎖CPU在調(diào)度線程的時(shí)候是在等待隊(duì)列里隨機(jī)挑選一個(gè)線程,由于這種隨機(jī)性所以是無(wú)法保證線程先到先得的(synchronized控制的鎖就是這種非公平鎖)。但這樣就會(huì)產(chǎn)生饑餓現(xiàn)象,即有些線程(優(yōu)先級(jí)較低的線程)可能永遠(yuǎn)也無(wú)法獲取CPU的執(zhí)行權(quán),優(yōu)先級(jí)高的線程會(huì)不斷的強(qiáng)制它的資源。那么如何解決饑餓問(wèn)題呢,這就需要公平鎖了。公平鎖可以保證線程按照時(shí)間的先后順序執(zhí)行,避免饑餓現(xiàn)象的產(chǎn)生。但公平鎖的效率比較低,因?yàn)橐獙?shí)現(xiàn)順序執(zhí)行,需要維護(hù)一個(gè)有序隊(duì)列。

條件對(duì)象Condition
假定一個(gè)線程已經(jīng)獲得鎖,將要執(zhí)行,但是他所需要的條件還沒(méi)有滿(mǎn)足(例如在余額不足的情況下取錢(qián)),便會(huì)造成有鎖卻不執(zhí)行,其他能夠提供滿(mǎn)足條件的線程(例如存錢(qián))卻只能等待,陷入僵局。
一個(gè)鎖對(duì)象可以有一個(gè)或多個(gè)相關(guān)的條件對(duì)象。你可以用newCondition 方法獲得一個(gè)條件對(duì)象。習(xí)慣上給每一個(gè)條件對(duì)象命名為可以反映它所表達(dá)的條件的名字。例如,在此設(shè)置一個(gè)條件對(duì)象來(lái)表達(dá)“ 余額充足”條件。

class Bank{
    private Condition sufficientFunds;
    ···
    public Bank(){
    ···
    sufficientFunds=bankLock.newCondition();
    }
}

如果transfer方法發(fā)現(xiàn)余額不足,它調(diào)用下面這個(gè)方法
sufficientFunds.await();
當(dāng)前線程現(xiàn)在被阻塞了,并放棄了鎖。我們希望這樣可以使得另一個(gè)線程可以進(jìn)行增加賬戶(hù)余額的操作。等待獲得鎖的線程和調(diào)用await 方法的線程存在本質(zhì)上的不同。一旦一個(gè)線程調(diào)用await方法,它進(jìn)人該條件的等待集。當(dāng)鎖可用時(shí),該線程不能馬上解除阻塞。相反,它處于阻塞狀態(tài),直到另一個(gè)線程調(diào)用同一條件上的signalAll 方法時(shí)為止。

synchronized關(guān)鍵字

在前面一節(jié)中,介紹了如何使用Lock 和Condition 對(duì)象。在進(jìn)一步深人之前,總結(jié)一下有關(guān)鎖和條件的關(guān)鍵之處:

  • 鎖用來(lái)保護(hù)代碼片段,任何時(shí)刻只能有一個(gè)線程執(zhí)行被保護(hù)的代碼。
  • 鎖可以管理試圖進(jìn)入被保護(hù)代碼段的線程。
  • 鎖可以擁有一個(gè)或多個(gè)相關(guān)的條件對(duì)象。
  • 每個(gè)條件對(duì)象管理那些已經(jīng)進(jìn)入被保護(hù)的代碼段但還不能運(yùn)行的線程。

Lock 和Condition 接口為程序設(shè)計(jì)人員提供了高度的鎖定控制。然而,大多數(shù)情況下,并不需要那樣的控制,并且可以使用一種嵌人到Java 語(yǔ)言?xún)?nèi)部的機(jī)制。

如果一個(gè)方法用synchronized 關(guān)鍵字聲明,那么對(duì)象的鎖將保護(hù)整個(gè)方法。也就是說(shuō),要調(diào)用該方法,線程必須獲得內(nèi)部的對(duì)象鎖。
換句話說(shuō)

public synchronized void method()
{
    method body
}

等價(jià)于

public void method()
{
    this.intrinsidock.1ock();
    try
    {
        method body
    }
    finally {
        this.intrinsicLock.unlockO;
    }
}

將靜態(tài)方法聲明為synchronized 也是合法的。如果調(diào)用這種方法,該方法獲得相關(guān)的類(lèi)對(duì)象的內(nèi)部鎖。例如,如果Bank類(lèi)有一個(gè)靜態(tài)同步的方法,那么當(dāng)該方法被調(diào)用時(shí),Bankxlass對(duì)象的鎖被鎖住。因此,沒(méi)有其他線程可以調(diào)用同一個(gè)類(lèi)的這個(gè)或任何其他的同步靜態(tài)方法。
靜態(tài)方法同步與非靜態(tài)方法同步區(qū)別:

  • 靜態(tài)同步:因此加入同步鎖以避免在該線程沒(méi)有完成操作之前,被其他線程的調(diào)用,從而保證了該變量的唯一性和準(zhǔn)確性。
  • 非靜態(tài)同步:鎖住的是該對(duì)象,類(lèi)的其中一個(gè)實(shí)例,當(dāng)該對(duì)象(僅僅是這一個(gè)對(duì)象)在不同線程中執(zhí)行這個(gè)同步方法時(shí),線程之間會(huì)形成互斥。達(dá)到同步效果,但如果不同線程同時(shí)對(duì)該類(lèi)的不同對(duì)象執(zhí)行這個(gè)同步方法時(shí),則線程之間不會(huì)形成互斥,因?yàn)樗麄儞碛械氖遣煌逆i。

內(nèi)部鎖和條件存在一些局限。包括:

  • 不能中斷一個(gè)正在試圖獲得鎖的線程。
  • 試圖獲得鎖時(shí)不能設(shè)定超時(shí)。
  • 每個(gè)鎖僅有單一的條件,可能是不夠的

synchronized和ReentrantLock的比較

區(qū)別:

  1. Lock是一個(gè)接口,而synchronized是Java中的關(guān)鍵字,synchronized是內(nèi)置的語(yǔ)言實(shí)現(xiàn);
  2. synchronized在發(fā)生異常時(shí),會(huì)自動(dòng)釋放線程占有的鎖,因此不會(huì)導(dǎo)致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時(shí),如果沒(méi)有主動(dòng)通過(guò)unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用Lock時(shí)需要在finally塊中釋放鎖;
  3. Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行,使用synchronized時(shí),等待的線程會(huì)一直等待下去,不能夠響應(yīng)中斷;
  4. 通過(guò)Lock可以知道有沒(méi)有成功獲取鎖,而synchronized卻無(wú)法辦到。
  5. Lock可以提高多個(gè)線程進(jìn)行讀操作的效率。

兩者在鎖的相關(guān)概念上區(qū)別:

  1. 可中斷鎖
    顧名思義,就是可以響應(yīng)中斷的鎖。
    在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。如果某一線程A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時(shí)間過(guò)長(zhǎng),線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
    lockInterruptibly()的用法體現(xiàn)了Lock的可中斷性。
  2. 公平鎖
    公平鎖即盡量以請(qǐng)求鎖的順序來(lái)獲取鎖。比如同是有多個(gè)線程在等待一個(gè)鎖,當(dāng)這個(gè)鎖被釋放時(shí),等待時(shí)間最久的線程(最先請(qǐng)求的線程)會(huì)獲得該鎖(并不是絕對(duì)的,大體上是這種順序),這種就是公平鎖。
    非公平鎖即無(wú)法保證鎖的獲取是按照請(qǐng)求鎖的順序進(jìn)行的。這樣就可能導(dǎo)致某個(gè)或者一些線程永遠(yuǎn)獲取不到鎖。
    在Java中,synchronized就是非公平鎖,它無(wú)法保證等待的線程獲取鎖的順序。ReentrantLock可以設(shè)置成公平鎖。
  3. 讀寫(xiě)鎖
    讀寫(xiě)鎖將對(duì)一個(gè)資源(比如文件)的訪問(wèn)分成了2個(gè)鎖,一個(gè)讀鎖和一個(gè)寫(xiě)鎖。
    正因?yàn)橛辛俗x寫(xiě)鎖,才使得多個(gè)線程之間的讀操作可以并發(fā)進(jìn)行,不需要同步,而寫(xiě)操作需要同步進(jìn)行,提高了效率。
    ReadWriteLock就是讀寫(xiě)鎖,它是一個(gè)接口,ReentrantReadWriteLock實(shí)現(xiàn)了這個(gè)接口。
    可以通過(guò)readLock()獲取讀鎖,通過(guò)writeLock()獲取寫(xiě)鎖。
  4. 綁定多個(gè)條件
    一個(gè)ReentrantLock對(duì)象可以同時(shí)綁定多個(gè)Condition對(duì)象,而在synchronized中,鎖對(duì)象的wait()和notify()或notifyAll()方法可以實(shí)現(xiàn)一個(gè)隱含的條件,如果要和多余一個(gè)條件關(guān)聯(lián)的時(shí)候,就不得不額外地添加一個(gè)鎖,而ReentrantLock則無(wú)須這么做,只需要多次調(diào)用new Condition()方法即可。

volatile域

volatile關(guān)鍵字為實(shí)例域的同步訪問(wèn)提供了一種免鎖機(jī)制。如果聲明一個(gè)域?yàn)関olatile,那么編譯器和虛擬機(jī)就知道該域是可能被另一個(gè)線程并發(fā)更新的。

注意:Volatile 變量不能提供原子性。例如
方法

public void flipDone() { done = !done; } // not atomic

不能確保翻轉(zhuǎn)域中的值。不能保證讀取、翻轉(zhuǎn)和寫(xiě)入不被中斷。

final

還有一種情況可以安全地訪問(wèn)一個(gè)共享域,即這個(gè)域聲明為final 時(shí)??紤]以下聲明:

final Map<String, Double〉accounts = new HashMap<>() ;

其他線程會(huì)在構(gòu)造函數(shù)完成構(gòu)造之后才看到這個(gè)accounts變量。

死鎖

產(chǎn)生條件

  1. 互斥條件:一個(gè)資源每次只能被一個(gè)線程使用。
  2. 請(qǐng)求與保持條件:一個(gè)線程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放。
  3. 不剝奪條件:線程已獲得的資源,在未使用完之前,不能強(qiáng)行剝奪。
  4. 循環(huán)等待條件:若干線程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。

有3種典型的死鎖類(lèi)型:

靜態(tài)的鎖順序死鎖

a和b兩個(gè)方法都需要獲得A鎖和B鎖。一個(gè)線程執(zhí)行a方法且已經(jīng)獲得了A鎖,在等待B鎖;另一個(gè)線程執(zhí)行了b方法且已經(jīng)獲得了B鎖,在等待A鎖。這種狀態(tài),就是發(fā)生了靜態(tài)的鎖順序死鎖。
解決靜態(tài)的鎖順序死鎖的方法就是:所有需要多個(gè)鎖的線程,都要以相同的順序來(lái)獲得鎖。

動(dòng)態(tài)的鎖順序死鎖

動(dòng)態(tài)的鎖順序死鎖是指兩個(gè)線程調(diào)用同一個(gè)方法時(shí),傳入的參數(shù)顛倒造成的死鎖。
如下代碼,一個(gè)線程調(diào)用了transferMoney方法并傳入?yún)?shù)accountA,accountB;另一個(gè)線程調(diào)用了transferMoney方法并傳入?yún)?shù)accountB,accountA。此時(shí)就可能發(fā)生在靜態(tài)的鎖順序死鎖中存在的問(wèn)題,即:第一個(gè)線程獲得了accountA鎖并等待accountB鎖,第二個(gè)線程獲得了accountB鎖并等待accountA鎖。

動(dòng)態(tài)的鎖順序死鎖解決方案如下:使用System.identifyHashCode來(lái)定義鎖的順序。確保所有的線程都以相同的順序獲得鎖。

協(xié)作對(duì)象之間發(fā)生的死鎖:

有時(shí),死鎖并不會(huì)那么明顯,比如兩個(gè)相互協(xié)作的類(lèi)之間的死鎖,比如下面的代碼:一個(gè)線程調(diào)用了Taxi對(duì)象的setLocation方法,另一個(gè)線程調(diào)用了Dispatcher對(duì)象的getImage方法。此時(shí)可能會(huì)發(fā)生,第一個(gè)線程持有Taxi對(duì)象鎖并等待Dispatcher對(duì)象鎖,另一個(gè)線程持有Dispatcher對(duì)象鎖并等待Taxi對(duì)象鎖。

上面的代碼中,我們?cè)诔钟墟i的情況下調(diào)用了外部的方法,這是非常危險(xiǎn)的(可能發(fā)生死鎖)。為了避免這種危險(xiǎn)的情況發(fā)生,我們使用開(kāi)放調(diào)用。如果調(diào)用某個(gè)外部方法時(shí)不需要持有鎖,我們稱(chēng)之為開(kāi)放調(diào)用。解決協(xié)作對(duì)象之間發(fā)生的死鎖:需要使用開(kāi)放調(diào)用,即避免在持有鎖的情況下調(diào)用外部的方法。

多線程

更多關(guān)于Java并發(fā)多線程請(qǐng)點(diǎn)擊Java進(jìn)階學(xué)習(xí)多線程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容