Java中的鎖

前言

Java中有很多的鎖, 本文將對鎖進行歸納總結, 便於自己以後學習理解。
在這裏插入圖片描述

樂觀鎖和悲觀鎖

樂觀鎖和悲觀鎖都是一種廣義上的概念,在Java和數據庫中都有對此概念的實際應用。

悲觀鎖認爲自己在使用數據的時候一定有別的線程來修改數據,因此在獲取數據的時候會先加鎖,確保數據不會被別的線程修改。在Java中synchronized關鍵字和Lock的實現類都是悲觀鎖。

樂觀鎖認爲自己在使用數據的時候不會有別的線程修改數據,所以不會添加鎖,只是在更新數據的時候去判斷之前有沒有別的線程更新了這個數據。如果這個數據沒有被更新,當前線程將自己修改的數據成功寫入,如果數據已經被其他線程更新,則根據不同的實現方式執行不同的操作(例如報錯或者自動重試)。樂觀鎖在Java中是通過使用無鎖編程來實現,最常採用的是CAS算法,Java原子類中的遞增操作就是通過CAS自旋實現的。
在這裏插入圖片描述
從上面的描述我們可以發現:
悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。

// ------------------------- 悲觀鎖的調用方式 -------------------------
// synchronized
public synchronized void testMethod() {
    // 操作同步資源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保證多個線程使用的是同一個鎖
public void modifyPublicResources() {
    lock.lock();
    // 操作同步資源
    lock.unlock();
}

// ------------------------- 樂觀鎖的調用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();  // 需要保證多個線程使用的是同一個AtomicInteger
atomicInteger.incrementAndGet(); //執行自增1

通過調用方式示例,我們可以發現悲觀鎖基本都是在顯式的鎖定之後再操作同步資源,而樂觀鎖則直接去操作同步資源。那麼,爲何樂觀鎖能夠做到不鎖定同步資源也可以正確的實現線程同步呢?我們通過介紹樂觀鎖的主要實現方式CAS的技術原理來爲大家解惑。

CAS全稱Compare And Swap(比較與交換),這是一種無鎖算法,在不使用鎖(沒有線程被阻塞)的情況下實現多線程之間的變量同步。java.util.concurrent包中的原子類就是通過CAS來實現樂觀鎖。
CAS操作涉及到三個操作數:
1.)需要讀寫的內存值V
2.)進行比較的值A
3.)要寫入的新值B
當且僅當V的值等於A時,CAS通過原子方式用新值B來更新V的值(比較和更新整體是一個原子操作),如果V的值不等於A,則不執行任何操作,更新操作是一個不斷重試的操作。
注意:CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。

自旋鎖和適應性自旋鎖

阻塞或喚醒一個Java線程需要操作系統切換CPU狀態來完成,這種狀態轉換需要耗費CPU不少時間,在許多場景中,同步資源的鎖定時間很短,爲了這一小段時間去切換線程,線程掛起和恢復現場的花費可能會讓系統得不償失。爲了讓當前線程稍等一下,我們需要讓當前線程進行自旋,如果在自旋完成之後前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷,這就是自旋鎖。
在這裏插入圖片描述
自旋鎖本身是有缺點的,它不能代替阻塞,自旋等待雖然避免了線程切換的開銷,但它要佔用處理器時間。如果鎖被佔用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被佔用的時間很長,那麼自旋的線程只會白白浪費處理器資源。自旋鎖的實現原理同樣也是CAS,通過看源碼我們會發現, 它就是一個do-while循環。
在這裏插入圖片描述
自適應意味着自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。

無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖

無鎖沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。CAS原理及應用即是無鎖的實現。

偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖,降低獲取鎖的代價。

輕量級鎖是指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。

重量級鎖是將除了擁有鎖的線程以外的線程都阻塞。

公平鎖 VS 非公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。

非公平鎖是多個線程加鎖時直接嘗試獲取鎖,獲取不到纔會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麼這個線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的線程先獲取鎖的場景。非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因爲線程有機率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處於等待隊列中的線程可能會餓死,或者等很久纔會獲得鎖。

可重入鎖 VS 非可重入鎖

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因爲之前已經獲取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。下面用示例代碼來進行分析:

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1執行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2執行...");
    }
}

在上面的代碼中,類中的兩個方法都是被內置鎖synchronized修飾的,doSomething()方法中調用doOthers()方法。因爲內置鎖是可重入的,所以同一個線程在調用doOthers()時可以直接獲得當前對象的鎖,進入doOthers()進行操作。

如果是一個不可重入鎖,那麼當前線程在調用doOthers()之前需要將執行doSomething()時獲取當前對象的鎖釋放掉,實際上該對象鎖已被當前線程所持有,且無法釋放。所以此時會出現死鎖。

獨享鎖 VS 共享鎖

獨享鎖也叫排他鎖,是指該鎖一次只能被一個線程所持有。如果線程T對數據A加上排它鎖後,則其他線程不能再對A加任何類型的鎖。獲得排它鎖的線程即能讀數據又能修改數據。JDK中的synchronized和JUC中Lock的實現類就是互斥鎖。

共享鎖是指該鎖可被多個線程所持有。如果線程T對數據A加上共享鎖後,則其他線程只能對A再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數據,不能修改數據。

在ReentrantReadWriteLock裏面,讀鎖和寫鎖的鎖主體都是Sync,但讀鎖和寫鎖的加鎖方式不一樣。讀鎖是共享鎖,寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因爲讀鎖和寫鎖是分離的。所以ReentrantReadWriteLock的併發性相比一般的互斥鎖有了很大提升。

轉載自美團技術團隊:
https://tech.meituan.com/Java_Lock.html

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