詳解Java線程安全

1、內存模型

高速緩存

由於CPU執行速度和內存數據讀寫速度差距很大,所以CPU每每包含高速緩存結構。 html

此處輸入圖片的描述
當程序在運行過程當中,會將運算須要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束以後,再將高速緩存中的數據刷新到主存當中。

緩存不一致問題

執行下面的代碼:java

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須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。bash

2、線程安全問題

產生緣由

從前面的分析,在併發編程(多線程編程)中,可能出現線程安全的問題:多線程

  • 多個線程在操做共享的數據。併發

  • 操做共享數據的線程代碼有多條。

  • 當一個線程在執行操做共享數據的多條代碼過程當中,其餘線程參與了運算。

併發的核心概念

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

  • 原子性:跟數據庫事務的原子性概念差很少,即一個操做(有可能包含有多個子操做)要麼所有執行(生效),要麼所有都不執行(都不生效)。

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

  • 可見性:當多個線程併發訪問共享變量時,一個線程對共享變量的修改,其它線程可以當即看到。

volatile關鍵字來保證可見性。

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

volatile在必定程序上保證順序性,另外還能夠經過synchronized來保證順序性。

3、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是如何保證併發編程中的安全的。


4、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

5、volatile

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

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

  • 保證可見性

線程寫入變量時不會把變量寫入緩存,而是直接把值刷新回主存。同時,其餘線程在讀取該共享變量的時候,會從主內存從新獲取值,而不是使用當前緩存中的值。(所以會帶來一部分性能損失)。注意:往主內存中寫入的操做不能保證原子性。

  • 禁止指令重排

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

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

6、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());
  }
複製代碼

7、比較

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標記的變量能夠被編譯器優化

7、死鎖問題

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

四個必要條件:

  • 互斥條件

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

  • 請求與保持條件

一個線程因請求資源而阻塞時,對已得到的資源保持不放。

  • 不剝奪條件

線程已得到的資源,在末使用完以前,不能強行剝奪。

  • 循環等待條件

若干線程之間造成一種頭尾相接的循環等待資源關係。

死鎖的例子

同步嵌套時,兩個線程互相鎖住,都不釋放,形成死鎖。 舉例: 建立兩個字符串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();
        }
    }
}
複製代碼

8、鎖的概念

在 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,之後再深刻學習吧,下面這個圖非常經典:

此處輸入圖片的描述


參考連接

相關文章
相關標籤/搜索