在面試官面前侃侃而談之對synchronized、Lock的深入理解

自律和變得更好是一個煎熬的過程

synchronized的缺陷

衆所周知,synchronized鎖是JAVA的關鍵字,按理說是JAVA語言內置的特性,那爲什麼還要使用Lock呢

我們先說一說synchronized,當一個方法或者代碼塊被synchronized修飾,並執行到此方法或者代碼塊時,獲取到鎖並執行,其他線程進來拿不到鎖就會一直等待,等待獲取到鎖的線程釋放鎖。而這裏獲取到鎖的線程釋放鎖只有2種情況

1:獲取到鎖的線程執行完畢,線程自動釋放對鎖的佔用。
2:線程執行過程中發生異常,JVM虛擬機將取消線程對鎖的佔用。

這裏插一手synchronized獲取鎖釋放鎖底層是怎麼操作的(底層都是通過monitor(監視器)來實現同步)

方法體加鎖:

方法體加鎖後,⽣成的字節碼⽂件中會多⼀個 ACC_SYNCHRONIZED 標誌位,當⼀個線程訪問⽅法時,會去檢查是否存在ACC_SYNCHRONIZED標識,如果存在,執⾏線程將先獲取monitor(監視器),獲取成功之後才能執⾏⽅法體,⽅法執⾏完後再釋放monitor。在⽅法執⾏期間,其他任何線程都⽆法再獲得同⼀個monitor對象,也叫隱式同步

代碼塊加鎖:

⽣成的字節碼⽂件會多出 monitorenter 和monitorexit 兩條指令,每個monitor維護着⼀個記錄着擁有次數的計數器, 未被擁有的monitor的該計數器爲0,當⼀個線程獲執⾏monitorenter後,該計數器⾃增1;當同⼀個線程執⾏monitorexit指令的時候,計數器再⾃減1。當計數器爲0的時候,monitor將被釋放.也叫顯式同步


好了,迴歸正題,獲取到鎖的線程只有這2種情況可以釋放鎖,其他的情況就會造成等待鎖的線程一直等待下去,這樣下去肯定是不行的,這時候肯定需要一直機制可以讓線程不要一直等待下去,最起碼可以等待一段時間進行關閉,或者能夠響應中斷。

再舉一個例子,讀寫場景不陌生吧,寫的時候別人不能寫也不能讀,但讀的時候大家可以一起讀,如果使用synchronized,那肯定也只有一個線程可以讀了,這時候是不是就需要一種機制可以讓大家一起讀。

那麼,Lock就出來了,不好意思synchronized,人家Lock這2種情況都可以解決,這波怎麼說?(不過,synchronized也不是一無是處,在某些場景比Lock好用的多,這裏暫且不提,只能說各有所用)。

Lock和ReentrantLock

那麼就到了介紹Lock的時候了,通過源碼可以知道,Lock是一個接口。

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

ReentrantLock,意思是“可重入鎖”。ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。

基本語法上,ReentrantLock與synchronized很相似,它們都具備一樣的線程重入特性,只是代碼寫法上有點區別而已。一個表現爲API層面的互斥鎖(Lock),一個表現爲原生語法層面的互斥鎖(synchronized)。

常用方法

內部提供了幾個方法,我們瞭解一下

1:lock()和unlock():
lock() 見名知意,獲取鎖。Lock最重要的就是自己手動獲取鎖,自己手動關閉鎖,因此就需要和unlock搭配使用,通常把unlock放到finally裏面,確保及時出異常也可以對鎖進行關閉。這裏就體現出沒有synchronized那麼方便了,synchronized全自動運行。

通常這麼搭配

ReentrantLock lock = new ReentrantLock();
//獲取鎖
lock.lock();
try {
    //執行業務代碼
} catch (Exception e) {
    e.printStackTrace();
}finally {
    //關閉鎖
    lock.unlock();
}

2:tryLock():
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回,在拿不到鎖時不會一直在那等待。
另一個方法是重載的tryLock(),參數是時間和時間單位,不容置疑,在拿不到鎖時會等待設定的時間。再時間期限之內還拿不到那隻能遺憾的返回false了,如果一開始就拿到了,或者在等待的時間段內拿到鎖,那麼返回true,適合對操作成功與否要求沒有那麼高的場景

通常這樣使用:

ReentrantLock lock = new ReentrantLock();
//拿不到鎖就等5秒,再拿不到就不拿了
if(lock.tryLock(5,TimeUnit.SECONDS)){
    try {
        //處理操作
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        //獲取到鎖則必須關閉
        lock.unlock();
    }
}

3:lockInterruptibly():
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就是說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。

由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。

因此lockInterruptibly()一般的使用形式如下:

ReentrantLock lock = new ReentrantLock();
lock.lockInterruptibly();
try {  
 //.....
}
finally {
    lock.unlock();
}  

4:newCondition():
這個對象就比較強大了。能夠精細的控制多線程的休眠與喚醒。對於同一個鎖,我們可以創建多個Condition,在不同的情況下使用不同的Condition。

比如,現在遇到一個問題,假如多線程讀/寫同一個緩衝區:當向緩衝區中寫入數據之後,喚醒"讀線程";當從緩衝區讀出數據之後,喚醒"寫線程";並且當緩衝區滿的時候,"寫線程"需要等待;當緩衝區爲空時,"讀線程"需要等待。

這時候你是不是想到的是Object的wait()和notify()方法。
如果採用Object類中的wait(), notify(),notifyAll()實現該緩衝區,當向緩衝區寫入數據之後需要喚醒"讀線程"時,不可能通過notify()或notifyAll()明確的指定喚醒"讀線程",而只能通過notifyAll喚醒所有線程(但是notifyAll無法區分喚醒的線程是讀線程,還是寫線程)。 但是,通過Condition,就能明確的指定喚醒讀線程。

這時候newCondition的強大之處就體現出來了。

現在有這樣一道題:多個線程按照順序調用,實現A–>B–>C三個線程啓動,要求A打印1次,B打印3次,C打印5次,緊接着,繼續A打印1次,B打印3次,C打印5次.

怎麼寫?

public class ConditionDemo {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();
            for (int i = 0; i < 2; i++) {
                new Thread(()->{
                    shareData.printf1();
                },String.valueOf(i)).start();
                new Thread(()->{
                    shareData.printf3();
                },String.valueOf(i+10)).start();
                new Thread(()->{
                    shareData.printf5();
                },String.valueOf(i+20)).start();
        }
    }
}
class ShareData{
	//    A1,B2,C3
    private volatile int count=1;
    private Lock lock=new ReentrantLock();
    //操作線程A
    private Condition condition1=lock.newCondition();
    //操作線程B
    private Condition condition2=lock.newCondition();
    //操作線程C
    private Condition condition3=lock.newCondition();
    
	//輸出一次
    public void printf1(){
        lock.lock();
        try {
            while (count!=1){
                condition1.await();
            }
            System.out.println("線程"+Thread.currentThread().getName()+"打印count="+count);
            count=2;
            //精確指定線程B結束休眠,開始操作
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void printf3(){
        lock.lock();
        try {
            while (count!=2){
                condition2.await();
            }
            System.out.println("線程"+Thread.currentThread().getName()+"打印count="+count);
            count=3;
            //精確指定線程C結束休眠,開始操作
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void printf5(){
        lock.lock();
        try {
            while (count!=3){
                condition3.await();
            }
            System.out.println("線程"+Thread.currentThread().getName()+"打印count="+count);
            count=1;
            //精確指定線程A結束休眠,開始操作
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

控制檯輸出:

線程0打印count=1
線程10打印count=2
線程20打印count=3
線程1打印count=1
線程11打印count=2
線程21打印count=3

體現出Condition的強大之處了吧

ReadWriteLock和ReentrantReadWriteLock

ReadWriteLock也是一個接口,在它裏面只定義了兩個方法:

一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。下面的ReentrantReadWriteLock實現了ReadWriteLock接口。

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReentrantReadWriteLock裏面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。

不過要注意的是,如果有一個線程已經佔用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖,如果要申請讀鎖,可可以申請到。如果有一個線程已經佔用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。

讀讀共存
讀寫互斥
寫寫互斥

ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
new Thread(()->{
    //獲取寫鎖
    readWriteLock.writeLock().lock();
    try {
        //進行寫操作
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        //關閉寫鎖
        readWriteLock.writeLock().unlock();
    }
}).start();
new Thread(()->{
    //獲取讀鎖
    readWriteLock.writeLock().lock();
    try {
        //進行讀操作
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        //關閉讀鎖
        readWriteLock.writeLock().unlock();
    }
}).start();

Lock和synchronized區別

1,原始構成:

Synchronized:
關鍵字,屬於JVM層面,底層通過Monitor對象來完成,其中wait/notify等方法也依賴於Monitor對象,只有在同步塊或方法中才能調wait/notify等方法

Lock:
具體類,是API層面的鎖

2,使用方法:

Synchronized:
不需要用戶去手動釋放鎖,當代碼執行完後系統會自動讓線程釋放對鎖的佔用

Lock:
需要用戶手動釋放鎖,若沒有手動釋放鎖,就有可能導致出現死鎖現象,需要lock和unlock配合try/finally語句塊來完成

3,中斷

Synchronized:
不可中斷,除非拋出異常或者正常運行完成

Lock:
可中斷
設置超時時間trylock(long timeout,TimeUnit unit)lockInterruptibly()放代碼塊中,調用interrupt()方法可中斷

4,公平:

Synchronized:
非公平鎖

Lock:
默認非公平鎖,構造方法可以傳入boolean值,true爲公平鎖,false爲非公平鎖

5,Condition

Synchronized:

Lock:
用來實現分組喚醒需要喚醒的線程們,可以精確喚醒,而不是像Synchronized要麼隨機喚醒一個線程要麼喚醒全部線程

synchronized鎖升級

在多線程併發編程中 synchronized 一直是元老級角色,很 多人都會稱呼它爲重量級鎖。但是,隨着 Java SE 1.6 對 synchronized 進行了各種優化之後,有些情況下它就並不 那麼重,Java SE 1.6 中爲了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖。

在這裏插入圖片描述
偏向鎖

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀的鎖記錄裏存儲偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需測試Mark Word裏線程ID是否爲當前線程。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要判斷偏向鎖的標識。如果標識被設置爲0(表示當前是無鎖狀態),則使用CAS競爭鎖;如果標識設置成1(表示當前是偏向鎖狀態),則嘗試使用CAS將對象頭的偏向鎖指向當前線程,觸發偏向鎖的撤銷。偏向鎖只有在競爭出現纔會釋放鎖。當其他線程嘗試競爭偏向鎖時,程序到達全局安全點後(沒有正在執行的代碼),它會查看Java對象頭中記錄的線程是否存活,如果沒有存活,那麼鎖對象被重置爲無鎖狀態,其它線程可以競爭將其設置爲偏向鎖;如果存活,那麼立刻查找該線程的棧幀信息,如果還是需要繼續持有這個鎖對象,那麼暫停當前線程,撤銷偏向鎖,升級爲輕量級鎖,如果線程1不再使用該鎖對象,那麼將鎖對象狀態設爲無鎖狀態,重新偏向新的線程。

由於有鎖撤銷的過程會消耗系統資源,所以,在鎖爭用特別激烈的時候,用偏向鎖未必效率高。還不如直接使用輕量級鎖。

輕量級鎖
線程在執行同步塊之前,JVM會先在當前線程的棧幀中創建用於存儲鎖記錄的空間,並將對象頭的MarkWord複製到鎖記錄中,即Displaced Mark Word。然後線程會嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。如果成功,當前線程獲得鎖。如果失敗,表示其他線程在競爭鎖,當前線程使用自旋來獲取鎖。當自旋次數達到一定次數時(1.6之後,出現了自適應自旋,JDk根據運行情況和每個線程運行情況自適應決定),鎖就會升級爲重量級鎖。

重量級鎖
重量級鎖就是通過內核來操作線程。因爲頻繁出現內核態與用戶態的切換,會嚴重影響性能。

注意:鎖只能升級不能降級,但是偏向鎖狀態可以被重置爲無鎖狀態。

流程圖
在這裏插入圖片描述

公平鎖和非公平鎖

非公平鎖:

簡單來說,就是多個線程獲取鎖並不一定按照申請鎖的順序,先來先嚐試佔有鎖
lock默認是非公平鎖,synchronized是非公平鎖

公平鎖:
多個線程按照申請鎖的順序來獲取鎖,先來先得

看一下ReentrantLock源碼,默認無參構造調用的是非公平鎖

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

參數爲true則公平鎖

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

看一下ReentrantLock公平鎖和非公平鎖工作流程

/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
     //非公平鎖獲取鎖
    final void lock() {
    	//進來就用CAS拿鎖
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
        	//拿鎖失敗,才使用公平鎖的邏輯
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
	//公平鎖則直接進去正常獲取鎖邏輯
    final void lock() {
        acquire(1);
    }
public final void acquire(int arg) {
	//嘗試獲取鎖
    if (!tryAcquire(arg) &&
    	//沒獲取到則加入等待隊列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //獲取到進入擁有鎖的流程
        selfInterrupt();
}
**
 * Fair version of tryAcquire.  Don't grant access unless
 * recursive call or no waiters or is first.
 */
 //嘗試獲取鎖的操作
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //獲取鎖狀態
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
        	//CAS獲取鎖
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

這麼看來,非公平鎖獲取鎖的過程中比公平鎖多獲取一次鎖,也就是剛開始直接用CAS操作
底層的方法也都是基於AQS的方法,進一步驗證了JUC是基於AQS實現的。

文章持續更新,可以微信搜索「 紳堂Style 」第一時間閱讀,回覆【資料】有我準備的面試題筆記。
GitHub https://github.com/dtt11111/Nodes 有總結面試完整考點、資料以及我的系列文章。歡迎Star。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章