首頁 >Java >java教程 >完全掌握Java鎖(圖文解析)

完全掌握Java鎖(圖文解析)

WBOY
WBOY轉載
2022-06-14 11:47:321965瀏覽

本篇文章為大家帶來了關於java的相關知識,其中主要介紹了關於java鎖的相關問題,包括了獨佔鎖、悲觀鎖、樂觀鎖、共享鎖等等內容,下面一起來看一下,希望對大家有幫助。

完全掌握Java鎖(圖文解析)

推薦學習:《java影片教學

樂觀鎖與悲觀鎖定

悲觀鎖

悲觀鎖定對應於生活中悲觀的人,悲觀的人總是想著事情往壞的方向發展。

舉個生活中的例子,假設廁所只有一個坑位了,悲觀鎖上廁所會第一時間把門反鎖上,這樣其他人上廁所只能在門外等候,這種狀態就是“阻塞”了。

回到程式碼世界中,一個共享資料加了悲觀鎖,那線程每次想操作這個數據前都會假設其他線程也可能會操作這個數據,所以每次操作前都會上鎖,這樣其他執行緒想操作這個資料拿不到鎖只能阻塞了。

完全掌握Java鎖(圖文解析)

在Java 語言中synchronizedReentrantLock等就是典型的悲觀鎖,還有一些使用了synchronized 關鍵字的容器類別如HashTable 等也是悲觀鎖的應用。

樂觀鎖

樂觀鎖 對應於生活中樂觀的人,樂觀的人總是想著事情往好的方向發展。

舉個人生的例子,假設廁所只有一個坑位了,樂觀鎖認為:這荒郊野外的,又沒有什麼人,不會有人搶我坑位的,每次關門上鎖多浪費時間,還是不加鎖好了。你看樂觀鎖就是天生樂觀!

回到程式碼世界中,樂觀鎖操作資料時不會上鎖,在更新的時候會判斷一下在此期間是否有其他執行緒去更新這個資料。

完全掌握Java鎖(圖文解析)

樂觀鎖定可以使用版本號機制CAS演算法實作。在 Java 語言中 java.util.concurrent.atomic套件下的原子類別就是使用CAS 樂觀鎖定實現的。

兩種鎖的使用場景

悲觀鎖定和樂觀鎖沒有孰優孰劣,有其各自適應的場景。

樂觀鎖適用於寫比較少(衝突比較小)的場景,因為不用上鎖、釋放鎖,省去了鎖的開銷,從而提升了吞吐量。

如果是寫多讀少的場景,即衝突比較嚴重,線程間競爭激勵,使用樂觀鎖就是導致線程不斷進行重試,這樣可能還降低了性能,這種場景下使用悲觀鎖就比較合適。

獨佔鎖定與共享鎖定

獨佔鎖定

#獨佔鎖定是指鎖一次只能被一個執行緒所持有。如果一個執行緒對資料加上排他鎖後,那麼其他執行緒就不能再對該資料加任何類型的鎖。取得獨佔鎖的執行緒即能讀取資料又能修改資料。

完全掌握Java鎖(圖文解析)

JDK中的synchronizedjava.util.concurrent(JUC)套件中Lock的實作類別就是獨佔鎖定。

共享鎖定

共享鎖定是指鎖定可被多個執行緒所持有。如果一個執行緒對資料加上共享鎖後,那麼其他執行緒只能對資料再加共享鎖,不能加獨佔鎖。取得共享鎖的執行緒只能讀數據,不能修改數據。

完全掌握Java鎖(圖文解析)

在 JDK 中 ReentrantReadWriteLock 是一種共享鎖定。

互斥鎖與讀寫鎖

互斥鎖

#互斥鎖是獨佔鎖的一種常規實現,是指某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。

完全掌握Java鎖(圖文解析)

互斥鎖一次只能有一個執行緒擁有互斥鎖,其他執行緒只有等待。

讀寫鎖定

讀寫鎖定是共享鎖定的一種具體實作。讀寫鎖管理一組鎖,一個是唯讀的鎖,一個是寫鎖。

讀鎖可以在沒有寫鎖的時候被多個執行緒同時持有,而寫鎖是獨佔的。寫鎖的優先權要高於讀鎖,一個獲得了讀鎖的執行緒必須能看到前一個釋放的寫鎖所更新的內容。

讀寫鎖比互斥鎖並發​​程度更高,每次只有一個寫線程,但是同時可以有多個線程並發讀。

完全掌握Java鎖(圖文解析)

在JDK 中定義了一個讀寫鎖定的介面:ReadWriteLock

public interface ReadWriteLock {
    /**
     * 获取读锁
     */
    Lock readLock();

    /**
     * 获取写锁
     */
    Lock writeLock();
}

ReentrantReadWriteLock 實作了ReadWriteLock接口,具體實作這裡不展開,後續會深入原始碼解析。

公平鎖定和非公平鎖定

公平鎖定

#公平鎖定是指多個執行緒依照申請鎖定的順序來取得鎖,這裡類似排隊買票,先來的人先買,後來的人在隊尾排著,這是公平的。

完全掌握Java鎖(圖文解析)

在java 中可以透過建構子初始化公平鎖定

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);

非公平鎖定

##非公平鎖是指多個執行緒取得鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先取得鎖,在高並發環境下,有可能造成優先權翻轉,或飢餓的狀態(某個執行緒一直沒有鎖)。

完全掌握Java鎖(圖文解析)

在 java 中 synchronized 關鍵字是非公平鎖,ReentrantLock預設為非公平鎖定。

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(false);
可重入鎖定

可重入鎖定又稱之為遞迴鎖,是指同一個執行緒在外層方法取得了鎖,在進入內層方法會自動取得鎖。

完全掌握Java鎖(圖文解析)

對於Java ReentrantLock而言, 他的名字就可以看出是一個可重入鎖。對於Synchronized而言,也是一個可重入鎖。

敲黑板:可重入鎖的一個好處是可一定程度避免死鎖。

以synchronized 為例,看一下下面的程式碼:

public synchronized void mehtodA() throws Exception{
 // Do some magic tings
 mehtodB();
}

public synchronized void mehtodB() throws Exception{
 // Do some magic tings
}
上面的程式碼中methodA 呼叫methodB,如果一個執行緒呼叫methodA 已經取得了鎖再去呼叫methodB 就不需要再取得鎖了,這就是可重入鎖的特性。如果不是可重入鎖的話,mehtodB 可能不會被目前執行緒執行,可能造成死鎖。

自旋鎖定

自旋鎖定是指線程在沒有獲得鎖定時不是被直接掛起,而是執行一個忙循環,這個忙循環就是所謂的自旋。

完全掌握Java鎖(圖文解析)

自旋鎖定的目的是為了減少執行緒被掛起的幾率,因為執行緒的掛起和喚醒也都是耗資源的操作。

如果鎖被另一個執行緒佔用的時間比較長,即使自旋了之後當前執行緒還是會被掛起,忙循環就會變成浪費系統資源的操作,反而降低了整體效能。因此自旋鎖是不適應鎖佔用時間長的並發情況的。

在Java 中,

AtomicInteger 類別有自旋的操作,我們看一下程式碼:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
CAS 操作如果失敗就會一直循環取得目前value 值然後重試。

另外自適應自旋鎖定也需要了解一下。

在JDK1.6又引入了自適應自旋,這個就比較智能了,自旋時間不再固定,由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。如果虛擬機器認為這次自旋也很有可能再次成功那就次序較多的時間,如果自旋很少成功,那麼以後可能就直接省略掉自旋過程,避免浪費處理器資源。

分段鎖定

分段鎖定 是一種鎖定的設計,並不是具體的一種鎖定。

分段鎖定設計目的是將鎖的粒度進一步細化,當操作不需要更新整個數組的時候,就僅針對數組中的一項進行加鎖操作。

完全掌握Java鎖(圖文解析)

在 Java 語言中 CurrentHashMap 底層就用了分段鎖,使用Segment,就可以進行並發使用了。

鎖定升級(無鎖|偏向鎖|輕量級鎖|重量級鎖)

JDK1.6 為了提升性能減少獲得鎖和釋放鎖所帶來的消耗,引入了4種鎖的狀態:

無鎖定偏向鎖定輕量級鎖定重量級鎖定,它會隨著多線程的競爭情況逐漸升級,但不能降級。

無鎖

無鎖狀態其實就是上面講的樂觀鎖,這裡不再贅述。

偏向鎖定#

Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。

偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。

轻量级锁

当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。

重量级锁

如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。

升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。

在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。这一过程在后续讲解 synchronized 关键字的原理时会详细介绍。

锁优化技术(锁粗化、锁消除)

锁粗化

锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。

举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作。

private static final Object LOCK = new Object();

for(int i = 0;i <p>经过<code>锁粗化</code>后就变成下面这个样子了:</p><pre class="brush:php;toolbar:false"> synchronized(LOCK){
     for(int i = 0;i <p><strong>锁消除</strong></p><p><code>锁消除</code>是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。</p><p>举个例子让大家更好理解。</p><pre class="brush:php;toolbar:false">public String test(String s1, String s2){
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(s1);
    stringBuffer.append(s2);
    return stringBuffer.toString();
}

上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来。

test 方法中三个变量s1, s2, stringBuffer, 它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的。

我们都知道 StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除

StringBuffer.class

// append 是同步方法
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

一张图总结:

前面讲了 Java 语言中各种各种的锁,最后再通过六个问题统一总结一下:

完全掌握Java鎖(圖文解析)

推荐学习:《java视频教程

以上是完全掌握Java鎖(圖文解析)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除