解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 獲取鎖AQS算法 (百度-美團)

 本篇文章主要講了lock的原理 就是AQS算法,還有個姊妹篇 講解synchronized的實現原理 也是阿里常常問的,html

參考:深刻分析Synchronized原理(阿里面試題)java

必定要看後面的文章,先說結論: node

非公平鎖tryAcquire的流程是:檢查state字段,若爲0,表示鎖未被佔用,那麼嘗試佔用,若不爲0,檢查當前鎖是否被本身佔用,若被本身佔用,則更新state字段,表示重入鎖的次數。若是以上兩點都沒有成功,則獲取鎖失敗,返回false。面試

 

還有其餘的鎖,若是想要了解,參考:JAVA鎖機制-可重入鎖,可中斷鎖,公平鎖,讀寫鎖,自旋鎖,算法

用synchronized實現ReentrantLock 美團面試題參考:使用synchronized 實現ReentrantLock(美團面試題目)編程

前幾天去百度面試,面試官問多線程如何解決併發問題,感受本身對lock的原理了解不夠,這裏對兩種方式synchronized和lock作個系統的總結:api

解決多線程的併發安全問題,java無非就是加鎖,具體就是兩個方法緩存

(1) Synchronized(java自帶的關鍵字)安全

(2) lock 可重入鎖 (可重入鎖這個包java.util.concurrent.locks 底下有兩個接口,分別對應兩個類實現了這個兩個接口: 多線程

       (a)lock接口, 實現的類爲:ReentrantLock類 可重入鎖;

       (b)readwritelock接口,實現類爲:ReentrantReadWriteLock 讀寫鎖)

也就是說有三種:

(1)synchronized 是互斥鎖;

(2)ReentrantLock 顧名思義 :可重入鎖

(3)ReentrantReadWriteLock :讀寫鎖

讀寫鎖特色:

a)多個讀者能夠同時進行讀
b)寫者必須互斥(只容許一個寫者寫,也不能讀者寫者同時進行)
c)寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)

總結來講,Lock和synchronized有如下幾點不一樣:

1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
2)當synchronized塊結束時,會自動釋放鎖,lock通常須要在finally中本身釋放。synchronized在發生異常時,會自動釋放線程佔有的鎖,所以不會致使死鎖現象發生;而Lock在發生異常時,若是沒有主動經過unLock()去釋放鎖,則極可能形成死鎖現象,所以使用Lock時須要在finally塊中釋放鎖;
3)lock等待鎖過程當中能夠用interrupt來終端等待,而synchronized只能等待鎖的釋放,不能響應中斷。
4)lock能夠經過trylock來知道有沒有獲取鎖,而synchronized不能; 

5. 當synchronized塊執行時,只能使用非公平鎖,沒法實現公平鎖,而lock能夠經過new ReentrantLock(true)設置爲公平鎖,從而在某些場景下提升效率。

六、LLock能夠提升多個線程進行讀操做的效率。(能夠經過readwritelock實現讀寫分離)
七、synchronized 鎖類型 可重入 不可中斷 非公平 而 lock 是: 可重入 可判斷 可公平(二者皆可) 
在性能上來講,若是競爭資源不激烈,二者的性能是差很少的,而當競爭資源很是激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。因此說,在具體使用時要根據適當狀況選擇。 

首先看一下Synchronized的原理:

一、synchronized

把代碼塊聲明爲 synchronized,有兩個重要後果,一般是指該代碼具備 原子性(atomicity)和 可見性(visibility)

實現原子性的算範爲CAS(Compare and Swap) 參考:Java多線程系列——原子類的實現(CAS算法)

(1) 原子性

原子性意味着個時刻,只有一個線程可以執行一段代碼,這段代碼經過一個monitor object保護。從而防止多個線程在更新共享狀態時相互衝突。

 (2)  可見性

可見性則更爲微妙,它要對付內存緩存和編譯器優化的各類反常行爲。啥是可見性呢?

答:它必須確保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的 。

做用:若是沒有同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這將引起許多嚴重問題。 

通常來講,線程以某種沒必要讓其餘線程當即能夠看到的方式(無論這些線程在寄存器中、在處理器特定的緩存中,仍是經過指令重排或者其餘編譯器優化),不受緩存變量值的約束,可是若是開發人員使用了同步,那麼運行庫將確保某一線程對變量所作的更新先於對現有synchronized 塊所進行的更新,當進入由同一監控器(lock)保護的另外一個synchronized 塊時,將馬上能夠看到這些對變量所作的更新。相似的規則也存在於volatile變量上。

——volatile只保證可見性,不保證原子性 

(3)synchronize的限制:

  1. 當線程嘗試獲取鎖的時候,若是獲取不到鎖會一直阻塞, 它沒法中斷一個正在等候得到鎖的線程;
  2. 若是獲取鎖的線程進入休眠或者阻塞,除非當前線程異常,不然其餘線程嘗試獲取鎖必須一直等待,也沒法經過投票獲得鎖,若是不想等下去,也就無法獲得鎖。

二、ReentrantLock (可重入鎖) 

何爲可重入(美團面試提問過此處):參考:如何理解ReentrantLock的可重入和互斥?

可重入的意思是某一個線程是否可屢次得到一個鎖,在繼承的狀況下,若是不是可重入的,那就造成死鎖了,好比遞歸調用本身的時候;,若是不能可重入,每次都獲取鎖不合適,好比synchronized就是可重入的,ReentrantLock也是可重入的

鎖的概念就不用多解釋了,當某個線程A已經持有了一個鎖,當線程B嘗試進入被這個鎖保護的代碼段的時候.就會被阻塞.而鎖的操做粒度是」線程」,而不是調用(至於爲何要這樣,下面解釋).同一個線程再次進入同步代碼的時候.可使用本身已經獲取到的鎖,這就是可重入鎖java裏面內置鎖(synchronize)和Lock(ReentrantLock)都是可重入的 

我本身寫了個例子:   

package entrantlock_test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class parent {
    
     protected Lock lock=new ReentrantLock();
     
     public void test(){
         lock.lock();
         try{
             System.out.println("Parent");
         }finally{
             lock.unlock();
         }
     }
     
     
}

class Sub extends parent{

    @Override
    public void test() {
        // TODO Auto-generated method stub
        lock.lock();
        try{
        super.test();
        System.out.println("Sub");
        
        }finally{
            lock.unlock();
        }
    }
    
    
}

public class LockTest{
    
    public static void main(String[] args){
        
        Sub s=new Sub();
        s.test();
        
    }
}
View Code

 

 2.1 . 爲何要可重入 

若是線程A繼續再次得到這個鎖呢?好比一個方法是synchronized,遞歸調用本身,那麼第一次已經得到了鎖,第二次調用的時候還能進入嗎? 直觀上固然須要能進入.這就要求必須是可重入的.可重入鎖又叫作遞歸鎖,否則就死鎖了。 

 它實現方式是:

爲每一個鎖關聯一個獲取計數器和一個全部者線程,當計數值爲0的時候,這個所就沒有被任何線程只有.當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,而且將獲取計數值置爲1,若是同一個線程再次獲取這個鎖,技術值將遞增,退出一次同步代碼塊,計算值遞減,當計數值爲0時,這個鎖就被釋放.ReentrantLock裏面有實現

其實也有不可重入鎖:這個還真有.Linux下的pthread_mutex_t鎖是默認是非遞歸的。能夠經過設置PTHREAD_MUTEX_RECURSIVE屬性,將pthread_mutex_t鎖設置爲遞歸鎖。若是要本身實現不可重入鎖,同可重入鎖,這個計數器只能爲1.或者0,再次進入的時候,發現已是1了,就進行阻塞.jdk裏面沒有默認的實現類.

Java.util.concurrent.lock 中的Lock 框架是鎖定的一個抽象,Lock彌補了synchronized的侷限,提供了更加細粒度的加鎖功能。  

ReentrantLock 類是惟一實現了Lock的類 ,它擁有與synchronized 相同的併發性和內存語義,可是添加了相似鎖投票定時鎖等候可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用狀況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 能夠花更少的時候來調度線程,把更多時間用在執行線程上。)  

用sychronized修飾的方法或者語句塊在代碼執行完以後鎖自動釋放,而是用Lock須要咱們手動釋放鎖,因此爲了保證鎖最終被釋放(發生異常狀況),要把互斥區放在try內,釋放鎖放在finally內!!  

Lock 接口api以下  

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

 

 lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。

unLock()方法是用來釋放鎖的。
在Lock中聲明瞭四個方法來獲取鎖,那麼這四個方法有何區別呢?

  首先lock()方法是日常使用得最多的一個方法,就是用來獲取鎖。若是鎖已被其餘線程獲取,則進行等待。

  因爲在前面講到若是採用Lock,必須主動去釋放鎖,而且在發生異常時,不會自動釋放鎖。所以通常來講,使用Lock必須在try{}catch{}塊中進行,而且將釋放鎖的操做放在finally塊中進行,以保證鎖必定被被釋放,防止死鎖的發生。一般使用Lock來進行同步的話,是如下面這種形式去使用的: 

Lock lock = ...;
lock.lock(); try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 }

     tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,若是獲取成功,則返回true,若是獲取失敗(即鎖已被其餘線程獲取),則返回false,也就說這個方法不管如何都會當即返回。在拿不到鎖時不會一直在那等待。

  tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不過區別在於這個方法在拿不到鎖時會等待必定的時間,在時間期限以內若是還拿不到鎖,就返回false。若是若是一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

  因此,通常狀況下經過tryLock來獲取鎖時是這樣使用的: 

Lock lock = ...;
if(lock.tryLock()) { try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 } }else { //若是不能獲取鎖,則直接作其餘事情 }

  

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

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

  所以lockInterruptibly()通常的使用形式以下: 

public void method() throws InterruptedException {
lock.lockInterruptibly();
try { 
//.....
}
finally {
lock.unlock();
} 
}

 注意,當一個線程獲取了鎖以後,是不會被interrupt()方法中斷的。單獨調用interrupt()方法不能中斷正在運行過程當中的線程,只能中斷阻塞過程當中的線程。

  所以當經過lockInterruptibly()方法獲取某個鎖時,若是不能獲取到,只有進行等待的狀況下,是能夠響應中斷的。

  而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是沒法被中斷的,只有一直等待下去。   

2 AQS

    AbstractQueuedSynchronizer簡稱AQS,是一個用於構建鎖和同步容器的框架。事實上concurrent包內許多類都是基於AQS構建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解決了在實現同步容器時設計的大量細節問題。

    AQS使用一個FIFO的隊列表示排隊等待鎖的線程,它維護一個status的變量,每一個節點維護一個waitstatus的變量,當線程獲取到鎖的時候,隊列的status置爲1,此線程執行完了,那麼它的waitstatus爲-1;隊列頭部的線程執行完畢以後,它會調用它的後繼的線程(百度面試)。

隊列頭節點稱做「哨兵節點」或者「啞節點」,它不與任何線程關聯。其餘的節點與等待線程關聯,每一個節點維護一個等待狀態waitStatus。如圖

     AQS中還有一個表示狀態的字段state,例如ReentrantLocky用它表示線程重入鎖的次數,Semaphore用它表示剩餘的許可數量,FutureTask用它表示任務的狀態。對state變量值的更新都採用CAS操做保證更新操做的原子性。

    AbstractQueuedSynchronizer繼承了AbstractOwnableSynchronizer,這個類只有一個變量:exclusiveOwnerThread,表示當前佔用該鎖的線程,而且提供了相應的get,set方法。

    理解AQS能夠幫助咱們更好的理解JCU包中的同步容器。

3 lock()與unlock()實現原理

        ReentrantLock是Lock的默認實現之一。那麼lock()和unlock()是怎麼實現的呢?首先咱們要弄清楚幾個概念

  • 可重入鎖。可重入鎖是指同一個線程能夠屢次獲取同一把鎖。ReentrantLock和synchronized都是可重入鎖。
  • 可中斷鎖。可中斷鎖是指線程嘗試獲取鎖的過程當中,是否能夠響應中斷。synchronized是不可中斷鎖,而ReentrantLock則提供了中斷功能。
  • 公平鎖與非公平鎖。公平鎖是指多個線程同時嘗試獲取同一把鎖時,獲取鎖的順序按照線程達到的順序,而非公平鎖則容許線程「插隊」。synchronized是非公平鎖,而ReentrantLock的默認實現是非公平鎖,可是也能夠設置爲公平鎖。
  • CAS操做(CompareAndSwap)。CAS操做簡單的說就是比較並交換。CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。不然,處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該位置的值。CAS 有效地說明了「我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。」 Java併發包(java.util.concurrent)中大量使用了CAS操做,涉及到併發的地方都調用了sun.misc.Unsafe類方法進行CAS操做。

    ReentrantLock提供了兩個構造器,分別是 

public ReentrantLock() {
    sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }

 

    默認構造器初始化爲NonfairSync對象,即非公平鎖,而帶參數的構造器能夠指定使用公平鎖和非公平鎖。由lock()和unlock的源碼能夠看到,它們只是分別調用了sync對象的lock()和release(1)方法。

    Sync是ReentrantLock的內部類,它的結構以下

 能夠看到Sync擴展了AbstractQueuedSynchronizer。

3.3 NonfairSync

    咱們從源代碼出發,分析非公平鎖獲取鎖和釋放鎖的過程。 

3.3.1 lock() 

    lock()源碼以下 

final void lock() {
    if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }

      首先用一個CAS操做,判斷state是不是0(表示當前鎖未被佔用),若是是0則把它置爲1,而且設置當前線程爲該鎖的獨佔線程,表示獲取鎖成功。當多個線程同時嘗試佔用同一個鎖時,CAS操做只能保證一個線程操做成功,剩下的只能乖乖的去排隊啦。

    「非公平」即體如今這裏,若是佔用鎖的線程剛釋放鎖,state置爲0,而排隊等待鎖的線程還未喚醒時,新來的線程就直接搶佔了該鎖,那麼就「插隊」了(請注意此處的非公平鎖是指新來的線程跟隊列頭部的線程競爭鎖,隊列其餘的線程仍是正常排隊,百度面試題)。

    若當前有三個線程去競爭鎖,假設線程A的CAS操做成功了,拿到了鎖開開心心的返回了,那麼線程B和C則設置state失敗,走到了else裏面。咱們往下看acquire。

acquire(arg)

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

代碼很是簡潔,可是背後的邏輯卻很是複雜,可見Doug Lea大神的編程功力。

 1. 第一步。嘗試去獲取鎖。若是嘗試獲取鎖成功,方法直接返回。

tryAcquire(arg) 

final boolean nonfairTryAcquire(int acquires) {
    //獲取當前線程
    final Thread current = Thread.currentThread(); //獲取state變量值 int c = getState(); if (c == 0) { //沒有線程佔用鎖 if (compareAndSetState(0, acquires)) { //佔用鎖成功,設置獨佔線程爲當前線程  setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { //當前線程已經佔用該鎖 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); // 更新state值爲新的重入次數  setState(nextc); return true; } //獲取鎖失敗 return false; }

 

    非公平鎖tryAcquire的流程是:檢查state字段,若爲0,表示鎖未被佔用,那麼嘗試佔用,若不爲0,檢查當前鎖是否被本身佔用,若被本身佔用,則更新state字段,表示重入鎖的次數。若是以上兩點都沒有成功,則獲取鎖失敗,返回false。

2. 第二步,入隊。因爲上文中提到線程A已經佔用了鎖,因此B和C執行tryAcquire失敗,而且入等待隊列。若是線程A拿着鎖死死不放,那麼B和C就會被掛起。

先看下入隊的過程。

先看addWaiter(Node.EXCLUSIVE) 

/**
 * 將新節點和當前線程關聯而且入隊列
 * @param mode 獨佔/共享
 * @return 新節點
 */
private Node addWaiter(Node mode) {
    //初始化節點,設置關聯線程和模式(獨佔 or 共享)
    Node node = new Node(Thread.currentThread(), mode); // 獲取尾節點引用 Node pred = tail; // 尾節點不爲空,說明隊列已經初始化過 if (pred != null) { node.prev = pred; // 設置新節點爲尾節點 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 尾節點爲空,說明隊列還未初始化,須要初始化head節點併入隊新節點  enq(node); return node; }

 B、C線程同時嘗試入隊列,因爲隊列還沒有初始化,tail==null,故至少會有一個線程會走到enq(node)。咱們假設同時走到了enq(node)裏。 

/**
 * 初始化隊列而且入隊新節點
 */
private Node enq(final Node node) {
    //開始自旋
    for (;;) { Node t = tail; if (t == null) { // Must initialize // 若是tail爲空,則新建一個head節點,而且tail指向head if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; // tail不爲空,將新節點入隊 if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }

 

這裏體現了經典的自旋+CAS組合來實現非阻塞的原子操做。因爲compareAndSetHead的實現使用了unsafe類提供的CAS操做,因此只有一個線程會建立head節點成功。假設線程B成功,以後B、C開始第二輪循環,此時tail已經不爲空,兩個線程都走到else裏面。假設B線程compareAndSetTail成功,那麼B就能夠返回了,C因爲入隊失敗還須要第三輪循環。最終全部線程均可以成功入隊。

     當B、C入等待隊列後,此時AQS隊列以下:

3. 第三步,掛起。B和C相繼執行acquireQueued(final Node node, int arg)。這個方法讓已經入隊的線程嘗試獲取鎖,若失敗則會被掛起。 

/**
 * 已經入隊的線程嘗試獲取鎖
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; //標記是否成功獲取鎖
    try { boolean interrupted = false; //標記線程是否被中斷過 for (;;) { final Node p = node.predecessor(); //獲取前驅節點 //若是前驅是head,即該結點已成老二,那麼便有資格去嘗試獲取鎖 if (p == head && tryAcquire(arg)) { setHead(node); // 獲取成功,將當前節點設置爲head節點 p.next = null; // 原head節點出隊,在某個時間點被GC回收 failed = false; //獲取成功 return interrupted; //返回是否被中斷過  } // 判斷獲取失敗後是否能夠掛起,若能夠則掛起 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 線程若被中斷,設置interrupted爲true interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

 

code裏的註釋已經很清晰的說明了acquireQueued的執行流程。假設B和C在競爭鎖的過程當中A一直持有鎖,那麼它們的tryAcquire操做都會失敗,所以會走到第2個if語句中。咱們再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都作了哪些事吧。 

/**
 * 判斷當前線程獲取鎖失敗以後是否須要掛起.
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //前驅節點的狀態
    int ws = pred.waitStatus; if (ws == Node.SIGNAL) // 前驅節點狀態爲signal,返回true return true; // 前驅節點狀態爲CANCELLED if (ws > 0) { // 從隊尾向前尋找第一個狀態不爲CANCELLED的節點 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 將前驅節點的狀態設置爲SIGNAL  compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } /** * 掛起當前線程,返回線程中斷狀態並重置 */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }

 

    線程入隊後可以掛起的前提是,它的前驅節點的狀態爲SIGNAL,它的含義是「Hi,前面的兄弟,若是你獲取鎖而且出隊後,記得把我喚醒!」。因此shouldParkAfterFailedAcquire會先判斷當前節點的前驅是否狀態符合要求,若符合則返回true,而後調用parkAndCheckInterrupt,將本身掛起。若是不符合,再看前驅節點是否>0(CANCELLED),如果那麼向前遍歷直到找到第一個符合要求的前驅,若不是則將前驅節點的狀態設置爲SIGNAL。

也就是說當隊列頭部的線程執行完了以後,這個線程會調用後面的隊列的第一個線程(百度面試)。

     整個流程中,若是前驅結點的狀態不是SIGNAL,那麼本身就不能安心掛起,須要去找個安心的掛起點,同時能夠再嘗試下看有沒有機會去嘗試競爭鎖。

    最終隊列可能會以下圖所示

  線程B和C都已經入隊,而且都被掛起。當線程A釋放鎖的時候,就會去喚醒線程B去獲取鎖啦。

3.3.2 unlock()

unlock相對於lock就簡單不少。源碼以下 

public void unlock() {
    sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

 

若是理解了加鎖的過程,那麼解鎖看起來就容易多了。流程大體爲先嚐試釋放鎖,若釋放成功,那麼查看頭結點的狀態是否爲SIGNAL,若是是則喚醒頭結點的下個節點關聯的線程,若是釋放失敗那麼返回false表示解鎖失敗。這裏咱們也發現了,每次都只喚起頭結點的下一個節點關聯的線程

   最後咱們再看下tryRelease的執行過程 

/**
 * 釋放當前線程佔用的鎖
 * @param releases
 * @return 是否釋放成功
 */
protected final boolean tryRelease(int releases) {
    // 計算釋放後state值
    int c = getState() - releases; // 若是不是當前線程佔用鎖,那麼拋出異常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // 鎖被重入次數爲0,表示釋放成功 free = true; // 清空獨佔線程 setExclusiveOwnerThread(null); } // 更新state值  setState(c); return free; }

 

這裏入參爲1。tryRelease的過程爲:當前釋放鎖的線程若不持有鎖,則拋出異常。若持有鎖,計算釋放後的state值是否爲0,若爲0表示鎖已經被成功釋放,而且則清空獨佔線程,最後更新state值,返回free。 

3.3.3 小結

    用一張流程圖總結一下非公平鎖的獲取鎖的過程。    

 
 

3.4 FairSync

    公平鎖和非公平鎖不一樣之處在於,公平鎖在獲取鎖的時候,不會先去檢查state狀態,而是直接執行aqcuire(1),這裏再也不贅述。    

4 超時機制

    在ReetrantLock的tryLock(long timeout, TimeUnit unit) 提供了超時獲取鎖的功能。它的語義是在指定的時間內若是獲取到鎖就返回true,獲取不到則返回false。這種機制避免了線程無限期的等待鎖釋放。那麼超時的功能是怎麼實現的呢?咱們仍是用非公平鎖爲例來一探究竟。

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

 仍是調用了內部類裏面的方法。咱們繼續向前探究  

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); }

 

這裏的語義是:若是線程被中斷了,那麼直接拋出InterruptedException。若是未中斷,先嚐試獲取鎖,獲取成功就直接返回,獲取失敗則進入doAcquireNanos。tryAcquire咱們已經看過,這裏重點看一下doAcquireNanos作了什麼。 

/**
 * 在有限的時間內去競爭鎖
 * @return 是否獲取成功
 */
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException { // 起始時間 long lastTime = System.nanoTime(); // 線程入隊 final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { // 又是自旋! for (;;) { // 獲取前驅節點 final Node p = node.predecessor(); // 若是前驅是頭節點而且佔用鎖成功,則將當前節點變成頭結點 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } // 若是已經超時,返回false if (nanosTimeout <= 0) return false; // 超時時間未到,且須要掛起 if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) // 阻塞當前線程直到超時時間到期 LockSupport.parkNanos(this, nanosTimeout); long now = System.nanoTime(); // 更新nanosTimeout nanosTimeout -= now - lastTime; lastTime = now; if (Thread.interrupted()) //相應中斷 throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }

 

doAcquireNanos的流程簡述爲:線程先入等待隊列,而後開始自旋,嘗試獲取鎖,獲取成功就返回,失敗則在隊列裏找一個安全點把本身掛起直到超時時間過時。這裏爲何還須要循環呢?由於當前線程節點的前驅狀態可能不是SIGNAL,那麼在當前這一輪循環中線程不會被掛起,而後更新超時時間,開始新一輪的嘗試 

三、讀寫鎖ReentrantReadWriteLock

接口 ReadWriteLock,有個實現類是ReentrantReadWriteLock

讀讀互不干擾,寫寫互斥,若是有讀也有寫,那麼寫線程要優先讀線程

對!讀取線程不該該互斥!

咱們能夠用讀寫鎖ReadWriteLock實現:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; 
class Data {        
    private int data;// 共享數據    
    private ReadWriteLock rwl = new ReentrantReadWriteLock();       
    public void set(int data) {    
        rwl.writeLock().lock();// 取到寫鎖    
        try {    
            System.out.println(Thread.currentThread().getName() + "準備寫入數據");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            this.data = data;    
            System.out.println(Thread.currentThread().getName() + "寫入" + this.data);    
        } finally {    
            rwl.writeLock().unlock();// 釋放寫鎖    
        }    
    }       
  
    public void get() {    
        rwl.readLock().lock();// 取到讀鎖    
        try {    
            System.out.println(Thread.currentThread().getName() + "準備讀取數據");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            System.out.println(Thread.currentThread().getName() + "讀取" + this.data);    
        } finally {    
            rwl.readLock().unlock();// 釋放讀鎖    
        }    
    }    
}  

 

與互斥鎖定相比,讀-寫鎖定容許對共享數據進行更高級別的併發訪問。雖然一次只有一個線程(writer 線程)能夠修改共享數據,但在許多狀況下,任何數量的線程能夠同時讀取共享數據(reader 線程) 

從理論上講,與互斥鎖定相比,使用讀-寫鎖定所容許的併發性加強將帶來更大的性能提升。

在實踐中,只有在多處理器上而且只在訪問模式適用於共享數據時,才能徹底實現併發性加強。——例如,某個最初用數據填充而且以後不常常對其進行修改的 collection,由於常常對其進行搜索(好比搜索某種目錄),因此這樣的 collection 是使用讀-寫鎖定的理想候選者。 

四、線程間通訊Condition

Condition能夠替代傳統的線程間通訊,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。

——爲何方法名不直接叫wait()/notify()/nofityAll()?由於Object的這幾個方法是final的,不可重寫!

 

傳統線程的通訊方式,Condition均可以實現。

注意,Condition是被綁定到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。 

Condition的強大之處在於它能夠爲多個線程間創建不一樣的Condition

看JDK文檔中的一個例子:假定有一個綁定的緩衝區,它支持 put 和 take 方法。若是試圖在空的緩衝區上執行take 操做,則在某一個項變得可用以前,線程將一直阻塞;若是試圖在滿的緩衝區上執行 put 操做,則在有空間變得可用以前,線程將一直阻塞。咱們喜歡在單獨的等待 set 中保存put 線程和take 線程,這樣就能夠在緩衝區中的項或空間變得可用時利用最佳規劃,一次只通知一個線程。可使用兩個Condition 實例來作到這一點。

——其實就是java.util.concurrent.ArrayBlockingQueue的功能

優勢:
假設緩存隊列中已經存滿,那麼阻塞的確定是寫線程,喚醒的確定是讀線程,相反,阻塞的確定是讀線程,喚醒的確定是寫線程。 

 

若是想查看 線程5個狀態 請參考:Java線程的5種狀態及切換(透徹講解)-京東面試

如下是補充的知識點:

一、線程與進程:

在開始以前先把進程與線程進行區分一下,一個程序最少須要一個進程,而一個進程最少須要一個線程。

線程是程序執行流的最小單位,而進程是系統進行資源分配和調度的一個獨立單位。  

 

2.java.util.concurrent.locks包經常使用類  

2.2 ReentrantLock
  ReentrantLock,意思是「可重入鎖」,ReentrantLock是惟一實現了Lock接口的類,而且ReentrantLock提供了更多的方法。
詳見:java.util.concurrent.locks.ReentrantLock ,再也不列舉了。

2.3 ReadWriteLock
接口,只定義了兩個方法:

Lock readLock();
Lock writeLock();
一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操做分開,分紅2個鎖來分配給線程,從而使得多個線程能夠同時進行讀操做。

2.4 ReentrantReadWriteLock
實現了ReadWriteLock接口。

下面嘗試寫個例子,表示ReadWriteLock和使用synchronized的區別。 

 
1.若是有一個線程已經佔用了讀鎖,則此時其餘線程若是要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。

2.若是有一個線程已經佔用了寫鎖,則此時其餘線程若是申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。 

 


參考:  lock與synchronized區別詳解

參考:  Synchronized與Lock的區別與應用場景

參考:lock和synchronized的同步區別與選擇 

參考:ReentrantLock實現原理

相關文章
相關標籤/搜索