詳解Java執行緒安全

Fan คิดถึง發表於2019-04-09

一、記憶體模型

快取記憶體

因為CPU執行速度和記憶體資料讀寫速度差距很大,因此CPU往往包含快取記憶體結構。

此處輸入圖片的描述
當程式在執行過程中,會將運算需要的資料從主存複製一份到CPU的快取記憶體當中,那麼CPU進行計算時就可以直接從它的快取記憶體讀取資料和向其中寫入資料,當運算結束之後,再將快取記憶體中的資料重新整理到主存當中。

快取不一致問題

執行下面的程式碼:

int i = 0;
i = i + 1;
複製程式碼

當執行緒執行這個語句時,會先從主存當中讀取i的值i = 0,然後複製一份到快取記憶體當中,然後CPU執行指令對i進行加1操作,然後將資料寫入快取記憶體,最後將快取記憶體中i最新的值重新整理到主存當中。

可能存在情況:初始時,兩個執行緒分別讀取i的值存入各自所在的CPU的快取記憶體當中,然後執行緒1進行加1操作,然後把i的最新值1寫入到記憶體。此時執行緒2的快取記憶體當中i的值還是0,進行加1操作之後,i的值為1,然後執行緒2把i的值寫入記憶體。

也就是說,如果一個變數在多個CPU中都存在快取(多執行緒情況),那麼就可能存在快取不一致的問題。

快取不一致的解決

一般有兩種解決辦法:

  • 匯流排加鎖

因為CPU和其他部件進行通訊都是通過匯流排來進行的,如果對匯流排加鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如記憶體),從而使得只能有一個CPU能使用這個變數的記憶體。

  • 快取一致性協議

由於在鎖住匯流排期間,其他CPU無法訪問記憶體,導致效率低下。所以就出現了快取一致性協議。最出名的就是Intel的MESI協議MESI協議保證了每個快取中使用的共享變數的副本是一致的。 MESI協議核心思想是:當CPU寫資料時,如果發現操作的變數是共享變數,即在其他CPU中也存在該變數的副本,會發出訊號通知其他CPU將該變數的快取行置為無效狀態,因此當其他CPU需要讀取這個變數時,發現自己快取中快取該變數的快取行是無效的,那麼它就會從記憶體重新讀取。

二、執行緒安全問題

產生原因

從前面的分析,在併發程式設計(多執行緒程式設計)中,可能出現執行緒安全的問題:

  • 多個執行緒在操作共享的資料。

  • 操作共享資料的執行緒程式碼有多條。

  • 當一個執行緒在執行操作共享資料的多條程式碼過程中,其他執行緒參與了運算。

併發的核心概念

三個核心概念:原子性、可見性、順序性。

  • 原子性:跟資料庫事務的原子性概念差不多,即一個操作(有可能包含有多個子操作)要麼全部執行(生效),要麼全部都不執行(都不生效)。

鎖和同步(同步方法和同步程式碼塊)、CAS(CPU級別的CAS指令cmpxchg)。

  • 可見性:當多個執行緒併發訪問共享變數時,一個執行緒對共享變數的修改,其它執行緒能夠立即看到。

volatile關鍵字來保證可見性。

  • 順序性:程式執行的順序按照程式碼的先後順序執行。因為處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證程式中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的-即指令重排序

volatile在一定程式上保證順序性,另外還可以通過synchronized來保證順序性。

三、Java物件頭的結構

Java物件可以作為併發程式設計中的鎖。而鎖實際上存在於Java物件頭裡。如果物件是陣列型別,則虛擬機器用 3 個 Word(字寬)儲存物件頭,如果物件是非陣列型別,則用 2 字寬儲存物件頭。在 64 位虛擬機器中,一字寬等於八位元組,即 64bit

Java 物件頭裡的 Mark Word 裡預設儲存物件的 HashCode,分代年齡和鎖標記位。32 位 JVM 的 Mark Word 的預設儲存結構如下:

|-|25 bit|4bit|偏向鎖標誌位(1bit)|鎖標誌位(2bit)| |::|::|::|::|::| |無鎖狀態|物件的hashCode|物件分代年齡| |01|

64 位JVM的儲存結構如下:

鎖狀態

25bit

31bit

1bit

4bit

1bit

2bit

        </td>
        <td>
        
        </td>
        <td>
        <p><span lang="EN-US">cms_free</span></p>
        </td>
        <td>
        <p><span>分代年齡<span lang="EN-US"></span></span></p>
        </td>
        <td colspan="2">
        <p><span>偏向鎖<span lang="EN-US"></span></span></p>
        </td>
        <td>
        <p><span>鎖標誌位<span lang="EN-US"></span></span></p>
        </td>
    </tr><tr><td>
        <p><span>無鎖<span lang="EN-US"></span></span></p>
        </td>
        <td>
        <p><span lang="EN-US">unused</span></p>
        </td>
        <td>
        <p><span lang="EN-US">hashCode</span></p>
        </td>
        <td>
        
        </td>
        <td>
        
        </td>
        <td>
        
        </td>
        <td colspan="2">
        <p><span lang="EN-US">01</span></p>
        </td>
    </tr><tr><td>
        <p><span>偏向鎖<span lang="EN-US"></span></span></p>
        </td>
        <td colspan="2">
        <p><span lang="EN-US">ThreadID(54bit) Epoch(2bit)</span></p>
        </td>
        <td>
        
        </td>
        <td>
        
        </td>
        <td>
        <p><span lang="EN-US">1</span></p>
        </td>
        <td colspan="2">
        <p><span lang="EN-US">01</span></p>
        </td>
    </tr></tbody></table>
複製程式碼

在執行期間 Mark Word 裡儲存的資料會隨著鎖標誌位的變化而變化。


在瞭解了相關概念後,接下來介紹Java是如何保證併發程式設計中的安全的。


四、synchronized

用法

  • 修飾同步程式碼塊

將多條操作共享資料的執行緒程式碼封裝起來,當有執行緒在執行這些程式碼的時候,其他執行緒時不可以參與運算的。必須要當前執行緒把這些程式碼都執行完畢後,其他執行緒才可以參與運算。

synchronized(物件)
{
需要被同步的程式碼 ;
}
複製程式碼
  • 修飾同步函式(方法)
修飾符 synchronized 返回值 方法名(){
}

複製程式碼
  • 修飾一個靜態的方法,其作用的範圍是整個靜態方法,作用的物件是這個類的所有物件;

  • 修飾一個類,其作用的範圍是synchronized後面括號括起來的部分,作用主的物件是這個類的所有物件。

synchronized的作用主要有三個: (1)確保執行緒互斥的訪問同步程式碼 (2)保證共享變數的修改能夠及時可見 (3)有效解決重排序問題。

鎖物件

  • 對於同步方法,鎖是當前例項物件
  • 對於靜態同步方法,鎖是當前物件的 Class 物件
  • 對於同步方法塊,鎖是 synchonized 括號裡配置的物件。

實現原理

在編譯的位元組碼中加入了兩條指令來進行程式碼的同步。

monitorenter :

每個物件有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:

  • 如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor的所有者。
  • 如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.
  • 如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。

monitorexit:

執行monitorexit的執行緒必須是objectref所對應的monitor的所有者。 指令執行時,monitor的進入數減1,如果減1後進入數為0,那執行緒退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor的所有權。

synchronized的語義底層是通過一個monitor的物件來完成,其實wait/notify等方法也依賴於monitor物件,這就是為什麼只有在同步的塊或者方法中才能呼叫wait/notify等方法,否則會丟擲java.lang.IllegalMonitorStateException的異常的原因。

好處和弊端

好處:解決了執行緒的安全問題。

弊端:相對降低了效率,因為同步外的執行緒的都會判斷同步鎖。獲得鎖和釋放鎖帶來效能消耗。

編譯器對synchronized優化

Java6 為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java6 裡鎖一共有四種狀態:無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級。

  • 偏向鎖:大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得。偏向鎖的目的是在某個執行緒獲得鎖之後(執行緒的id會記錄在物件的Mark Wod中),消除這個執行緒鎖重入(CAS)的開銷,看起來讓這個執行緒得到了偏護。

  • 輕量級鎖(CAS):輕量級鎖是由偏向鎖升級來的,偏向鎖執行在一個執行緒進入同步塊的情況下,當第二個執行緒加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖;輕量級鎖的意圖是在沒有多執行緒競爭的情況下,通過CAS操作嘗試將MarkWord更新為指向LockRecord的指標,減少了使用重量級鎖的系統互斥量產生的效能消耗。

  • 重量級鎖:虛擬機器使用CAS操作嘗試將MarkWord更新為指向LockRecord的指標,如果更新成功表示執行緒就擁有該物件的鎖;如果失敗,會檢查MarkWord是否指向當前執行緒的棧幀,如果是,表示當前執行緒已經擁有這個鎖;如果不是,說明這個鎖被其他執行緒搶佔,此時膨脹為重量級鎖。

鎖狀態對應的Mark Word

以32位JVM為例:

鎖狀態

25 bit

4bit

1bit

2bit

23bit

2bit

是否是偏向鎖

鎖標誌位

輕量級鎖

指向棧中鎖記錄的指標

00

重量級鎖

指向互斥量(重量級鎖)的指標

10

GC標記

11

偏向鎖

執行緒ID

Epoch

物件分代年齡

1

01

五、volatile

volatile是Java中的一個關鍵字,用來修飾共享變數(類的成員變數、類的靜態成員變數)。

被修飾的變數包含兩層語義:

  • 保證可見性

執行緒寫入變數時不會把變數寫入快取,而是直接把值重新整理回主存。同時,其他執行緒在讀取該共享變數的時候,會從主記憶體重新獲取值,而不是使用當前快取中的值。(因此會帶來一部分效能損失)。注意:往主記憶體中寫入的操作不能保證原子性。

  • 禁止指令重排

禁止指令重排序有兩層意思:  1)當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;  2)在進行指令優化時,不能將在對volatile變數訪問的語句放在其後面執行,也不能把volatile變數後面的語句放到其前面執行。

**底層實現:**觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令

六、Lock

應用場景

如果一個程式碼塊被synchronized修飾了,當一個執行緒獲取了對應的鎖,並執行該程式碼塊時,其他執行緒便只能一直等待,等待獲取鎖的執行緒釋放鎖,而這裡獲取鎖的執行緒釋放鎖只會有兩種情況

  • 獲取鎖的執行緒執行完了該程式碼塊,然後執行緒釋放對鎖的佔有;
  • 執行緒執行發生異常,此時JVM會讓執行緒自動釋放鎖。

如果這個獲取鎖的執行緒由於要等待IO或者其他原因(比如呼叫sleep方法)被阻塞了,但是又沒有釋放鎖,會讓程式效率很差。

因此就需要有一種機制可以不讓等待的執行緒一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。

原始碼分析

與Lock相關的介面和類位於J.U.Cjava.util.concurrent.locks包下。

此處輸入圖片的描述

(1)Lock介面

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(): 有返回值的獲取鎖。注意返回型別是boolean,如果獲取鎖的時候鎖被佔用就返回false,否則返回truetryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待引數時間。 lockInterruptibly():當通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則這個執行緒能夠響應中斷,即中斷執行緒的等待狀態。也就使說,當兩個執行緒同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時執行緒A獲取到了鎖,而執行緒B只有在等待,那麼對執行緒B呼叫threadB.interrupt()方法能夠中斷執行緒B的等待過程。

注意:當一個執行緒獲取了鎖之後,是不會被interrupt()方法中斷的。因為本身在前面的文章中講過單獨呼叫interrupt()方法不能中斷正在執行過程中的執行緒,只能中斷阻塞過程中的執行緒。因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。用synchronized修飾的話,當一個執行緒處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。

  • 釋放鎖 unlock():釋放鎖。

(2)ReentrantLock類

ReentrantLock,意思是“可重入鎖”。ReentrantLock是唯一實現了Lock介面的類,並且ReentrantLock提供了更多的方法,基於AQS(AbstractQueuedSynchronizer)來實現的。

並且,ConcurrentHashMap並沒有採用synchronized進行控制,而是使用了ReentrantLock

  • 構造方法 ReentrantLock 分為公平鎖非公平鎖,可以通過構造方法來指定具體型別:
public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼
  • 獲取鎖
public void lock() {
    sync.lock();
}
複製程式碼

sync是一個abstract內部類:

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;
    abstract void lock();
複製程式碼

lock()方法用的是構造得到的FairSync物件,即sync的實現類。

public ReentrantLock() {
    sync = new NonfairSync();
}
//刪去一些方法
static final class NonfairSync extends Sync {
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
複製程式碼

compareAndSetStateAQS的一個方法,也就是基於CAS操作。

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

嘗試進一步獲取鎖(呼叫繼承自父類syncfinal方法):

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    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");
        setState(nextc);
        return true;
    }
    return false;
}
複製程式碼

首先會判斷 AQS 中的 state 是否等於 0,0表示目前沒有其他執行緒獲得鎖,當前執行緒就可以嘗試獲取鎖。如果 state 大於 0 時,說明鎖已經被獲取了,則需要判斷獲取鎖的執行緒是否為當前執行緒(ReentrantLock 支援重入),是則需要將 state + 1,並將值更新。

如果 tryAcquire(arg) 獲取鎖失敗,則需要用addWaiter(Node.EXCLUSIVE) 將當前執行緒寫入佇列中。寫入之前需要將當前執行緒包裝為一個 Node物件(addWaiter(Node.EXCLUSIVE))

即回到:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
複製程式碼
  • 釋放鎖
公平鎖和非公平鎖的釋放流程都是一樣的:
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;
}

//嘗試釋放鎖
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}        
複製程式碼

(3)ReadWriteLock介面ReentrantReadWriteLock類

  • 定義
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}
複製程式碼

ReentrantLock中,執行緒之間的同步都是互斥的,不管是讀操作還是寫操作,但是在一些場景中讀操作是可以並行進行的,只有寫操作才是互斥的,這種情況雖然也可以使用ReentrantLock來解決,但是在效能上也會損失,ReadWriteLock就是用來解決這個問題的。

  • 實現-ReentrantReadWriteLock類

ReentrantReadWriteLock中分別定義了讀鎖和寫鎖,與ReentrantLock類似,讀鎖和寫鎖的功能也是通過Sync實現的,Sync存在公平和非公平兩種實現方式,不同的是表示鎖狀態的state的定義,在ReentrantReadWriteLock中具體定義如下:

static final int SHARED_SHIFT     = 16;
  static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
  static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
  static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

  //獲取讀鎖的佔有次數
  static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
  //獲取寫鎖的佔有次數
  static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  
  //執行緒的id和對應執行緒獲取的讀鎖的數量
  static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = Thread.currentThread().getId();
  }
  
  //執行緒變數儲存執行緒和執行緒中獲取的讀寫的數量
  static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
      return new HoldCounter();
    }
  }
  
  private transient ThreadLocalHoldCounter readHolds;
  //快取最後一個獲取讀鎖的執行緒
  private transient HoldCounter cachedHoldCounter;
  //儲存第一個獲取讀鎖的執行緒
  private transient Thread firstReader = null;  
  private transient int firstReaderHoldCount; 

複製程式碼

其中,包含兩個靜態內部類:ReadLock()WriteLock(),都實現了Lock介面

獲取讀鎖

  • 如果不存線上程持有寫鎖,則獲取讀鎖成功。
  • 如果其他執行緒持有寫鎖,則獲取讀鎖失敗。
  • 如本執行緒持有寫鎖,並且不存在等待寫鎖的其他執行緒,則獲取讀鎖成功。
  • 如本執行緒持有寫鎖,並且存在等待寫鎖的其他執行緒,則如果本執行緒已經持有讀鎖,則獲取讀鎖成功,如果不能存在讀鎖,則此次獲取讀鎖失敗。

獲取寫鎖

  • 判斷是否有執行緒持有鎖,包括讀鎖和寫鎖,如果有,則執行步驟2,否則步驟3
  • 如果寫鎖為空(此時由於1步驟判斷存在鎖,則存在持有讀鎖的執行緒),或者持有寫鎖的不是本執行緒,直接返回失敗,如果寫鎖數量大於MAX_COUNT,返回失敗,否則更新state,並且返回true
  • 如果需要寫鎖堵塞判斷,或者CAS失敗直接返回false,否則設定持有寫鎖的執行緒為本執行緒,並且返回true
  • 通過writerShouldBlock寫鎖堵塞判斷
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
  }
//判斷是否堵塞
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
      ((s = h.next) == null || s.thread != Thread.currentThread());
  }
複製程式碼

七、比較

Lock和synchronized

synchronized是基於JVM層面實現的,而Lock是基於JDK層面實現的。Lock需要lockrelease,比synchronized複雜,但Lock可以做更細粒度的鎖,支援獲取超時、獲取中斷,這是synchronized所不具備的。Lock的實現主要有ReentrantLockReadLockWriteLock,讀讀共享,寫寫互斥,讀寫互斥。

  • Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現;

  • synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

  • Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;

  • 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。

  • Lock可以提高多個執行緒進行讀操作的效率。   

  • Lock實現和synchronized不一樣,後者是一種悲觀鎖,它膽子很小,它很怕有人和它搶吃的,所以它每次吃東西前都把自己關起來。而Lock底層其實是CAS 樂觀鎖的體現,它無所謂,別人搶了它吃的,它重新去拿吃的就好啦,所以它很樂觀。底層主要靠volatileCAS操作實現的。

synchronized和volatile

  • volatile本質是在告訴jvm當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主存中讀取;

  • synchronized則是鎖定當前變數,只有當前執行緒可以訪問該變數,其他執行緒被阻塞住。

  • volatile僅能使用在變數級別;synchronized則可以使用在變數、方法、和類級別的

  • volatile僅能實現變數的修改可見性,不能保證原子性;而synchronized則可以保證變數的修改可見性和原子性

  • volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞。

  • volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化

七、死鎖問題

死鎖有四個必要條件,打破一個即可去除死鎖。

四個必要條件:

  • 互斥條件

一個資源每次只能被一個程式使用。

  • 請求與保持條件

一個執行緒因請求資源而阻塞時,對已獲得的資源保持不放。

  • 不剝奪條件

執行緒已獲得的資源,在末使用完之前,不能強行剝奪。

  • 迴圈等待條件

若干執行緒之間形成一種頭尾相接的迴圈等待資源關係。

死鎖的例子

同步巢狀時,兩個執行緒互相鎖住,都不釋放,造成死鎖。 舉例: 建立兩個字串a和b,再建立兩個執行緒A和B,讓每個執行緒都用synchronized鎖住字串(A先鎖a,再去鎖b;B先鎖b,再鎖a),如果A鎖住a,B鎖住b,A就沒辦法鎖住b,B也沒辦法鎖住a,這時就陷入了死鎖。

public class DeadLock {
    public static String obj1 = "obj1";
    public static String obj2 = "obj2";
    public static void main(String[] args){
        Thread a = new Thread(new Lock1());
        Thread b = new Thread(new Lock2());
        a.start();
        b.start();
    }    
}
class Lock1 implements Runnable{
    @Override
    public void run(){
        try{
            System.out.println("Lock1 running");
            while(true){
                synchronized(DeadLock.obj1){
                    System.out.println("Lock1 lock obj1");
                    Thread.sleep(3000);//獲取obj1後先等一會兒,讓Lock2有足夠的時間鎖住obj2
                    synchronized(DeadLock.obj2){
                        System.out.println("Lock1 lock obj2");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
class Lock2 implements Runnable{
    @Override
    public void run(){
        try{
            System.out.println("Lock2 running");
            while(true){
                synchronized(DeadLock.obj2){
                    System.out.println("Lock2 lock obj2");
                    Thread.sleep(3000);
                    synchronized(DeadLock.obj1){
                        System.out.println("Lock2 lock obj1");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
複製程式碼

八、鎖的概念

在 java 中鎖的實現主要有兩類:內部鎖 synchronized(物件內建的monitor鎖)和顯示鎖java.util.concurrent.locks.Lock

  • 可重入鎖

指的是同一執行緒外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響,執行物件中所有同步方法不用再次獲得鎖。synchronizedLock都具備可重入性。

  • 可中斷鎖

synchronized就不是可中斷鎖,而Lock是可中斷鎖。

  • 公平鎖

按等待獲取鎖的執行緒的等待時間進行獲取,等待時間長的具有優先獲取鎖權利。synchronized就是非公平鎖;對於ReentrantLockReentrantReadWriteLock,它預設情況下是非公平鎖,但是可以設定為公平鎖。

  • 讀寫鎖

對資源讀取和寫入的時候拆分為2部分處理,讀的時候可以多執行緒一起讀,寫的時候必須同步地寫。ReadWriteLock就是讀寫鎖,它是一個介面,ReentrantReadWriteLock實現了這個介面。

  • 自旋鎖

讓執行緒去執行一個無意義的迴圈,迴圈結束後再去重新競爭鎖,如果競爭不到繼續迴圈,迴圈過程中執行緒會一直處於running狀態,但是基於JVM的執行緒排程,會讓出時間片,所以其他執行緒依舊有申請鎖和釋放鎖的機會。自旋鎖省去了阻塞鎖的時間空間(佇列的維護等)開銷,但是長時間自旋就變成了“忙式等待”,忙式等待顯然還不如阻塞鎖。所以自旋的次數一般控制在一個範圍內,例如10,100等,在超出這個範圍後,自旋鎖會升級為阻塞鎖。

  • 獨佔鎖

是一種悲觀鎖synchronized就是一種獨佔鎖,會導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖。

  • 樂觀鎖

每次不加鎖,假設沒有衝突去完成某項操作,如果因為衝突失敗就重試,直到成功為止。

  • 悲觀鎖

導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖。

關於JUC

包含了兩個子包:atomic以及lock,另外在concurrent下的阻塞佇列以及executors,以後再深入學習吧,下面這個圖很是經典:

此處輸入圖片的描述


參考連結

相關文章