主頁 > 後端開發 > 深入理解JAVA并發鎖

深入理解JAVA并發鎖

2020-11-10 23:33:28 後端開發

深入理解 Java 并發鎖

1. 并發鎖簡介

確保執行緒安全最常見的做法是利用鎖機制(Locksychronized)來對共享資料做互斥同步,這樣在同一個時刻,只有一個執行緒可以執行某個方法或者某個代碼塊,那么操作必然是原子性的,執行緒安全的,

在作業、面試中,經常會聽到各種五花八門的鎖,聽的人云里霧里,鎖的概念術語很多,它們是針對不同的問題所提出的,通過簡單的梳理,也不難理解,

1.1. 可重入鎖

可重入鎖,顧名思義,指的是執行緒可以重復獲取同一把鎖,即同一個執行緒在外層方法獲取了鎖,在進入內層方法會自動獲取鎖,

可重入鎖可以在一定程度上避免死鎖,

  • ReentrantLock 、ReentrantReadWriteLock 是可重入鎖,這點,從其命名也不難看出,
  • synchronized 也是一個可重入鎖,

【示例】synchronized 的可重入示例

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

上面的代碼就是一個典型場景:如果使用的鎖不是可重入鎖的話,setB 可能不會被當前執行緒執行,從而造成死鎖,

【示例】ReentrantLock 的可重入示例

class Task {

    private int value;
    private final Lock lock = new ReentrantLock();

    public Task() {
        this.value = https://www.cnblogs.com/hehe199316/p/0;
    }

    public int get() {
        // 獲取鎖
        lock.lock();
        try {
            return value;
        } finally {
            // 保證鎖能釋放
            lock.unlock();
        }
    }

    public void addOne() {
        // 獲取鎖
        lock.lock();
        try {
            // 注意:此處已經成功獲取鎖,進入 get 方法后,又嘗試獲取鎖,
            // 如果鎖不是可重入的,會導致死鎖
            value = https://www.cnblogs.com/hehe199316/p/1 + get();
        } finally {
            // 保證鎖能釋放
            lock.unlock();
        }
    }

}

1.2. 公平鎖與非公平鎖

  • 公平鎖 - 公平鎖是指 多執行緒按照申請鎖的順序來獲取鎖,
  • 非公平鎖 - 非公平鎖是指 多執行緒不按照申請鎖的順序來獲取鎖 ,這就可能會出現優先級反轉(后來者居上)或者饑餓現象(某執行緒總是搶不過別的執行緒,導致始終無法執行),

公平鎖為了保證執行緒申請順序,勢必要付出一定的性能代價,因此其吞吐量一般低于非公平鎖,

公平鎖與非公平鎖 在 Java 中的典型實作:

  • synchronized 只支持非公平鎖,
  • ReentrantLock 、ReentrantReadWriteLock,默認是非公平鎖,但支持公平鎖,

1.3. 獨享鎖與共享鎖

獨享鎖與共享鎖是一種廣義上的說法,從實際用途上來看,也常被稱為互斥鎖與讀寫鎖,

  • 獨享鎖 - 獨享鎖是指 鎖一次只能被一個執行緒所持有,
  • 共享鎖 - 共享鎖是指 鎖可被多個執行緒所持有,

獨享鎖與共享鎖在 Java 中的典型實作:

  • synchronized 、ReentrantLock 只支持獨享鎖,
  • ReentrantReadWriteLock 其寫鎖是獨享鎖,其讀鎖是共享鎖,讀鎖是共享鎖使得并發讀是非常高效的,讀寫,寫讀 ,寫寫的程序是互斥的,

1.4. 悲觀鎖與樂觀鎖

樂觀鎖與悲觀鎖不是指具體的什么型別的鎖,而是處理并發同步的策略,

  • 悲觀鎖 - 悲觀鎖對于并發采取悲觀的態度,認為:不加鎖的并發操作一定會出問題,悲觀鎖適合寫操作頻繁的場景,
  • 樂觀鎖 - 樂觀鎖對于并發采取樂觀的態度,認為:不加鎖的并發操作也沒什么問題,對于同一個資料的并發操作,是不會發生修改的,在更新資料的時候,會采用不斷嘗試更新的方式更新資料,樂觀鎖適合讀多寫少的場景,

悲觀鎖與樂觀鎖在 Java 中的典型實作:

  • 悲觀鎖在 Java 中的應用就是通過使用 synchronized 和 Lock 顯示加鎖來進行互斥同步,這是一種阻塞同步,

  • 樂觀鎖在 Java 中的應用就是采用 CAS 機制(CAS 操作通過 Unsafe 類提供,但這個類不直接暴露為 API,所以都是間接使用,如各種原子類),

1.5. 偏向鎖、輕量級鎖、重量級鎖

所謂輕量級鎖與重量級鎖,指的是鎖控制粒度的粗細,顯然,控制粒度越細,阻塞開銷越小,并發性也就越高,

Java 1.6 以前,重量級鎖一般指的是 synchronized ,而輕量級鎖指的是 volatile

Java 1.6 以后,針對 synchronized 做了大量優化,引入 4 種鎖狀態: 無鎖狀態、偏向鎖、輕量級鎖和重量級鎖,鎖可以單向的從偏向鎖升級到輕量級鎖,再從輕量級鎖升級到重量級鎖 ,

  • 偏向鎖 - 偏向鎖是指一段同步代碼一直被一個執行緒所訪問,那么該執行緒會自動獲取鎖,降低獲取鎖的代價,

  • 輕量級鎖 - 是指當鎖是偏向鎖的時候,被另一個執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能,

  • 重量級鎖 - 是指當鎖為輕量級鎖的時候,另一個執行緒雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖,重量級鎖會讓其他申請的執行緒進入阻塞,性能降低,

1.6. 分段鎖

分段鎖其實是一種鎖的設計,并不是具體的一種鎖,所謂分段鎖,就是把鎖的物件分成多段,每段獨立控制,使得鎖粒度更細,減少阻塞開銷,從而提高并發性,這其實很好理解,就像高速公路上的收費站,如果只有一個收費口,那所有的車只能排成一條隊繳費;如果有多個收費口,就可以分流了,

Hashtable 使用 synchronized 修飾方法來保證執行緒安全性,那么面對執行緒的訪問,Hashtable 就會鎖住整個物件,所有的其它執行緒只能等待,這種阻塞方式的吞吐量顯然很低,

Java 1.7 以前的 ConcurrentHashMap 就是分段鎖的典型案例,ConcurrentHashMap 維護了一個 Segment 陣列,一般稱為分段桶,

final Segment<K,V>[] segments;

當有執行緒訪問 ConcurrentHashMap 的資料時,ConcurrentHashMap 會先根據 hashCode 計算出資料在哪個桶(即哪個 Segment),然后鎖住這個 Segment

1.7. 顯示鎖和內置鎖

Java 1.5 之前,協調對共享物件的訪問時可以使用的機制只有 synchronized 和 volatile,這兩個都屬于內置鎖,即鎖的申請和釋放都是由 JVM 所控制,

Java 1.5 之后,增加了新的機制:ReentrantLockReentrantReadWriteLock ,這類鎖的申請和釋放都可以由程式所控制,所以常被稱為顯示鎖,

注意:如果不需要 ReentrantLockReentrantReadWriteLock 所提供的高級同步特性,應該優先考慮使用 synchronized ,理由如下:

  • Java 1.6 以后,synchronized 做了大量的優化,其性能已經與 ReentrantLockReentrantReadWriteLock 基本上持平,
  • 從趨勢來看,Java 未來更可能會優化 synchronized ,而不是 ReentrantLockReentrantReadWriteLock ,因為 synchronized 是 JVM 內置屬性,它能執行一些優化,
  • ReentrantLockReentrantReadWriteLock 申請和釋放鎖都是由程式控制,如果使用不當,可能造成死鎖,這是很危險的,

以下對比一下顯示鎖和內置鎖的差異:

  • 主動獲取鎖和釋放鎖
    • synchronized 不能主動獲取鎖和釋放鎖,獲取鎖和釋放鎖都是 JVM 控制的,
    • ReentrantLock 可以主動獲取鎖和釋放鎖,(如果忘記釋放鎖,就可能產生死鎖),
  • 回應中斷
    • synchronized 不能回應中斷,
    • ReentrantLock 可以回應中斷,
  • 超時機制
    • synchronized 沒有超時機制,
    • ReentrantLock 有超時機制,ReentrantLock 可以設定超時時間,超時后自動釋放鎖,避免一直等待,
  • 支持公平鎖
    • synchronized 只支持非公平鎖,
    • ReentrantLock 支持非公平鎖和公平鎖,
  • 是否支持共享
    • 被 synchronized 修飾的方法或代碼塊,只能被一個執行緒訪問(獨享),如果這個執行緒被阻塞,其他執行緒也只能等待
    • ReentrantLock 可以基于 Condition 靈活的控制同步條件,
  • 是否支持讀寫分離
    • synchronized 不支持讀寫鎖分離;
    • ReentrantReadWriteLock 支持讀寫鎖,從而使阻塞讀寫的操作分開,有效提高并發性,

2. Lock 和 Condition

2.1. 為何引入 Lock 和 Condition

并發編程領域,有兩大核心問題:一個是互斥,即同一時刻只允許一個執行緒訪問共享資源;另一個是同步,即執行緒之間如何通信、協作,這兩大問題,管程都是能夠解決的,Java SDK 并發包通過 Lock 和 Condition 兩個介面來實作管程,其中 Lock 用于解決互斥問題,Condition 用于解決同步問題,

synchronized 是管程的一種實作,既然如此,何必再提供 Lock 和 Condition,

JDK 1.6 以前,synchronized 還沒有做優化,性能遠低于 Lock,但是,性能不是引入 Lock 的最重要因素,真正關鍵在于:synchronized 使用不當,可能會出現死鎖,

synchronized 無法通過破壞不可搶占條件來避免死鎖,原因是 synchronized 申請資源的時候,如果申請不到,執行緒直接進入阻塞狀態了,而執行緒進入阻塞狀態,啥都干不了,也釋放不了執行緒已經占有的資源,

與內置鎖 synchronized 不同的是,Lock 提供了一組無條件的、可輪詢的、定時的以及可中斷的鎖操作,所有獲取鎖、釋放鎖的操作都是顯式的操作,

  • 能夠回應中斷,synchronized 的問題是,持有鎖 A 后,如果嘗試獲取鎖 B 失敗,那么執行緒就進入阻塞狀態,一旦發生死鎖,就沒有任何機會來喚醒阻塞的執行緒,但如果阻塞狀態的執行緒能夠回應中斷信號,也就是說當我們給阻塞的執行緒發送中斷信號的時候,能夠喚醒它,那它就有機會釋放曾經持有的鎖 A,這樣就破壞了不可搶占條件了,
  • 支持超時,如果執行緒在一段時間之內沒有獲取到鎖,不是進入阻塞狀態,而是回傳一個錯誤,那這個執行緒也有機會釋放曾經持有的鎖,這樣也能破壞不可搶占條件,
  • 非阻塞地獲取鎖,如果嘗試獲取鎖失敗,并不進入阻塞狀態,而是直接回傳,那這個執行緒也有機會釋放曾經持有的鎖,這樣也能破壞不可搶占條件,

2.2. Lock 介面

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() - 獲取鎖,
  • unlock() - 釋放鎖,
  • tryLock() - 嘗試獲取鎖,僅在呼叫時鎖未被另一個執行緒持有的情況下,才獲取該鎖,
  • tryLock(long time, TimeUnit unit) - 和 tryLock() 類似,區別僅在于限定時間,如果限定時間內未獲取到鎖,視為失敗,
  • lockInterruptibly() - 鎖未被另一個執行緒持有,且執行緒沒有被中斷的情況下,才能獲取鎖,
  • newCondition() - 回傳一個系結到 Lock 物件上的 Condition 實體,

2.3. Condition

Condition 實作了管程模型里面的條件變數,

前文中提過 Lock 介面中 有一個 newCondition() 方法用于回傳一個系結到 Lock 物件上的 Condition 實體,Condition 是什么?有什么作用?本節將一一講解,

在單執行緒中,一段代碼的執行可能依賴于某個狀態,如果不滿足狀態條件,代碼就不會被執行(典型的場景,如:if ... else ...),在并發環境中,當一個執行緒判斷某個狀態條件時,其狀態可能是由于其他執行緒的操作而改變,這時就需要有一定的協調機制來確保在同一時刻,資料只能被一個執行緒鎖修改,且修改的資料狀態被所有執行緒所感知,

Java 1.5 之前,主要是利用 Object 類中的 waitnotifynotifyAll 配合 synchronized 來進行執行緒間通信 ,

waitnotifynotifyAll 需要配合 synchronized 使用,不適用于 Lock,而使用 Lock 的執行緒,彼此間通信應該使用 Condition ,這可以理解為,什么樣的鎖配什么樣的鑰匙,內置鎖(synchronized)配合內置條件佇列(waitnotifynotifyAll ),顯式鎖(Lock)配合顯式條件佇列(Condition ),

Condition 的特性

Condition 介面定義如下:

public interface Condition {
    void await() throws InterruptedException;
    void awaitUninterruptibly();
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;
    void signal();
    void signalAll();
}

其中,awaitsignalsignalAll 與 waitnotifynotifyAll 相對應,功能也相似,除此以外,Condition 相比內置條件佇列( waitnotifynotifyAll ),提供了更為豐富的功能:

  • 每個鎖(Lock)上可以存在多個 Condition,這意味著鎖的狀態條件可以有多個,
  • 支持公平的或非公平的佇列操作,
  • 支持可中斷的條件等待,相關方法:awaitUninterruptibly() ,
  • 支持可定時的等待,相關方法:awaitNanos(long) 、await(long, TimeUnit)awaitUntil(Date)

Condition 的用法

這里以 Condition 來實作一個消費者、生產者模式,

產品類

class Message {

    private final Lock lock = new ReentrantLock();

    private final Condition producedMsg = lock.newCondition();

    private final Condition consumedMsg = lock.newCondition();

    private String message;

    private boolean state;

    private boolean end;

    public void consume() {
        //lock
        lock.lock();
        try {
            // no new message wait for new message
            while (!state) { producedMsg.await(); }

            System.out.println("consume message : " + message);
            state = false;
            // message consumed, notify waiting thread
            consumedMsg.signal();
        } catch (InterruptedException ie) {
            System.out.println("Thread interrupted - viewMessage");
        } finally {
            lock.unlock();
        }
    }

    public void produce(String message) {
        lock.lock();
        try {
            // last message not consumed, wait for it be consumed
            while (state) { consumedMsg.await(); }

            System.out.println("produce msg: " + message);
            this.message = message;
            state = true;
            // new message added, notify waiting thread
            producedMsg.signal();
        } catch (InterruptedException ie) {
            System.out.println("Thread interrupted - publishMessage");
        } finally {
            lock.unlock();
        }
    }

    public boolean isEnd() {
        return end;
    }

    public void setEnd(boolean end) {
        this.end = end;
    }

}

消費者

class MessageConsumer implements Runnable {

    private Message message;

    public MessageConsumer(Message msg) {
        message = msg;
    }

    @Override
    public void run() {
        while (!message.isEnd()) { message.consume(); }
    }

}

生產者

class MessageProducer implements Runnable {

    private Message message;

    public MessageProducer(Message msg) {
        message = msg;
    }

    @Override
    public void run() {
        produce();
    }

    public void produce() {
        List<String> msgs = new ArrayList<>();
        msgs.add("Begin");
        msgs.add("Msg1");
        msgs.add("Msg2");

        for (String msg : msgs) {
            message.produce(msg);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        message.produce("End");
        message.setEnd(true);
    }

}

測驗

public class LockConditionDemo {

    public static void main(String[] args) {
        Message msg = new Message();
        Thread producer = new Thread(new MessageProducer(msg));
        Thread consumer = new Thread(new MessageConsumer(msg));
        producer.start();
        consumer.start();
    }
}

3. ReentrantLock

ReentrantLock 類是 Lock 介面的具體實作,與內置鎖 synchronized 相同的是,它是一個可重入鎖,

3.1. ReentrantLock 的特性

ReentrantLock 的特性如下:

  • ReentrantLock 提供了與 synchronized 相同的互斥性、記憶體可見性和可重入性,
  • ReentrantLock 支持公平鎖和非公平鎖(默認)兩種模式,
  • ReentrantLock 實作了 Lock 介面,支持了 synchronized 所不具備的靈活性,
    • synchronized 無法中斷一個正在等待獲取鎖的執行緒
    • synchronized 無法在請求獲取一個鎖時無休止地等待

3.2. ReentrantLock 的用法

前文了解了 ReentrantLock 的特性,接下來,我們要講述其具體用法,

ReentrantLock 的構造方法

ReentrantLock 有兩個構造方法:

public ReentrantLock() {}
public ReentrantLock(boolean fair) {}
  • ReentrantLock() - 默認構造方法會初始化一個非公平鎖(NonfairSync);
  • ReentrantLock(boolean) - new ReentrantLock(true) 會初始化一個公平鎖(FairSync),

lock 和 unlock 方法

  • lock() - 無條件獲取鎖,如果當前執行緒無法獲取鎖,則當前執行緒進入休眠狀態不可用,直至當前執行緒獲取到鎖,如果該鎖沒有被另一個執行緒持有,則獲取該鎖并立即回傳,將鎖的持有計數設定為 1,
  • unlock() - 用于釋放鎖,

:bell: 注意:請務必牢記,獲取鎖操作 lock() 必須在 try catch 塊外進行,并且將釋放鎖操作 unlock() 放在 finally 塊中進行,以保證鎖一定被被釋放,防止死鎖的發生,

示例:ReentrantLock 的基本操作

public class ReentrantLockDemo {

    public static void main(String[] args) {
        Task task = new Task();
        MyThread tA = new MyThread("Thread-A", task);
        MyThread tB = new MyThread("Thread-B", task);
        MyThread tC = new MyThread("Thread-C", task);
        tA.start();
        tB.start();
        tC.start();
    }

    static class MyThread extends Thread {

        private Task task;

        public MyThread(String name, Task task) {
            super(name);
            this.task = task;
        }

        @Override
        public void run() {
            task.execute();
        }

    }

    static class Task {

        private ReentrantLock lock = new ReentrantLock();

        public void execute() {
            lock.lock();
            try {
                for (int i = 0; i < 3; i++) {
                    System.out.println(lock.toString());

                    // 查詢當前執行緒 hold 住此鎖的次數
                    System.out.println("\t holdCount: " + lock.getHoldCount());

                    // 查詢正等待獲取此鎖的執行緒數
                    System.out.println("\t queuedLength: " + lock.getQueueLength());

                    // 是否為公平鎖
                    System.out.println("\t isFair: " + lock.isFair());

                    // 是否被鎖住
                    System.out.println("\t isLocked: " + lock.isLocked());

                    // 是否被當前執行緒持有鎖
                    System.out.println("\t isHeldByCurrentThread: " + lock.isHeldByCurrentThread());

                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                lock.unlock();
            }
        }

    }

}

輸出結果:

java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-A]
	 holdCount: 1
	 queuedLength: 2
	 isFair: false
	 isLocked: true
	 isHeldByCurrentThread: true
java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-C]
	 holdCount: 1
	 queuedLength: 1
	 isFair: false
	 isLocked: true
	 isHeldByCurrentThread: true
// ...

tryLock 方法

與無條件獲取鎖相比,tryLock 有更完善的容錯機制,

  • tryLock() - 可輪詢獲取鎖,如果成功,則回傳 true;如果失敗,則回傳 false,也就是說,這個方法無論成敗都會立即回傳,獲取不到鎖(鎖已被其他執行緒獲取)時不會一直等待,
  • tryLock(long, TimeUnit) - 可定時獲取鎖,和 tryLock() 類似,區別僅在于這個方法在獲取不到鎖時會等待一定的時間,在時間期限之內如果還獲取不到鎖,就回傳 false,如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則回傳 true,

示例:ReentrantLock 的 tryLock() 操作

修改上個示例中的 execute() 方法

public void execute() {
    if (lock.tryLock()) {
        try {
            for (int i = 0; i < 3; i++) {
               // 略...
            }
        } finally {
            lock.unlock();
        }
    } else {
        System.out.println(Thread.currentThread().getName() + " 獲取鎖失敗");
    }
}

示例:ReentrantLock 的 tryLock(long, TimeUnit) 操作

修改上個示例中的 execute() 方法

public void execute() {
    try {
        if (lock.tryLock(2, TimeUnit.SECONDS)) {
            try {
                for (int i = 0; i < 3; i++) {
                    // 略...
                }
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " 獲取鎖失敗");
        }
    } catch (InterruptedException e) {
        System.out.println(Thread.currentThread().getName() + " 獲取鎖超時");
        e.printStackTrace();
    }
}

lockInterruptibly 方法

  • lockInterruptibly() - 可中斷獲取鎖,可中斷獲取鎖可以在獲得鎖的同時保持對中斷的回應,可中斷獲取鎖比其它獲取鎖的方式稍微復雜一些,需要兩個 try-catch 塊(如果在獲取鎖的操作中拋出了 InterruptedException ,那么可以使用標準的 try-finally 加鎖模式),
    • 舉例來說:假設有兩個執行緒同時通過 lock.lockInterruptibly() 獲取某個鎖時,若執行緒 A 獲取到了鎖,則執行緒 B 只能等待,若此時對執行緒 B 呼叫 threadB.interrupt() 方法能夠中斷執行緒 B 的等待程序,由于 lockInterruptibly() 的宣告中拋出了例外,所以 lock.lockInterruptibly() 必須放在 try 塊中或者在呼叫 lockInterruptibly() 的方法外宣告拋出 InterruptedException

:bell: 注意:當一個執行緒獲取了鎖之后,是不會被 interrupt() 方法中斷的,單獨呼叫 interrupt() 方法不能中斷正在運行狀態中的執行緒,只能中斷阻塞狀態中的執行緒,因此當通過 lockInterruptibly() 方法獲取某個鎖時,如果未獲取到鎖,只有在等待的狀態下,才可以回應中斷,

示例:ReentrantLock 的 lockInterruptibly() 操作

修改上個示例中的 execute() 方法

public void execute() {
    try {
        lock.lockInterruptibly();

        for (int i = 0; i < 3; i++) {
            // 略...
        }
    } catch (InterruptedException e) {
        System.out.println(Thread.currentThread().getName() + "被中斷");
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}

newCondition 方法

newCondition() - 回傳一個系結到 Lock 物件上的 Condition 實體,

3.3. ReentrantLock 的原理

ReentrantLock 的可見性

class X {
  private final Lock rtl =
  new ReentrantLock();
  int value;
  public void addOne() {
    // 獲取鎖
    rtl.lock();
    try {
      value+=1;
    } finally {
      // 保證鎖能釋放
      rtl.unlock();
    }
  }
}

ReentrantLock,內部持有一個 volatile 的成員變數 state,獲取鎖的時候,會讀寫 state 的值;解鎖的時候,也會讀寫 state 的值(簡化后的代碼如下面所示),也就是說,在執行 value+=1 之前,程式先讀寫了一次 volatile 變數 state,在執行 value+=1 之后,又讀寫了一次 volatile 變數 state,根據相關的 Happens-Before 規則:

  1. 順序性規則:對于執行緒 T1,value+=1 Happens-Before 釋放鎖的操作 unlock();
  2. volatile 變數規則:由于 state = 1 會先讀取 state,所以執行緒 T1 的 unlock() 操作 Happens-Before 執行緒 T2 的 lock() 操作;
  3. 傳遞性規則:執行緒 T1 的 value+=1 Happens-Before 執行緒 T2 的 lock() 操作,

ReentrantLock 的資料結構

閱讀 ReentrantLock 的原始碼,可以發現它有一個核心欄位:

private final Sync sync;
  • sync - 內部抽象類 ReentrantLock.Sync 物件,Sync 繼承自 AQS,它有兩個子類:
  • ReentrantLock.FairSync - 公平鎖,
  • ReentrantLock.NonfairSync - 非公平鎖,

查看原始碼可以發現,ReentrantLock 實作 Lock 介面其實是呼叫 ReentrantLock.FairSync 或 ReentrantLock.NonfairSync 中各自的實作,這里不一一列舉,

ReentrantLock 的獲取鎖和釋放鎖

ReentrantLock 獲取鎖和釋放鎖的介面,從表象看,是呼叫 ReentrantLock.FairSync 或 ReentrantLock.NonfairSync 中各自的實作;從本質上看,是基于 AQS 的實作,

仔細閱讀原始碼很容易發現:

  • void lock() 呼叫 Sync 的 lock() 方法,

  • void lockInterruptibly() 直接呼叫 AQS 的 獲取可中斷的獨占鎖 方法 lockInterruptibly()

  • boolean tryLock() 呼叫 Sync 的 nonfairTryAcquire() ,

  • boolean tryLock(long time, TimeUnit unit) 直接呼叫 AQS 的 獲取超時等待式的獨占鎖 方法 tryAcquireNanos(int arg, long nanosTimeout)

  • void unlock() 直接呼叫 AQS 的 釋放獨占鎖 方法 release(int arg) ,

直接呼叫 AQS 介面的方法就不再贅述了,其原理在 [AQS 的原理](#AQS 的原理) 中已經用很大篇幅進行過講解,

nonfairTryAcquire 方法原始碼如下:

// 公平鎖和非公平鎖都會用這個方法區嘗試獲取鎖
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
         // 如果同步狀態為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;
}

處理流程很簡單:

  • 如果同步狀態為 0,設定同步狀態設為 acquires,并設定當前執行緒為排它執行緒,然后回傳 true,獲取鎖成功,
  • 如果同步狀態不為 0 且當前執行緒為排它執行緒,設定同步狀態為當前狀態值+acquires 值,然后回傳 true,獲取鎖成功,
  • 否則,回傳 false,獲取鎖失敗,

公平鎖和非公平鎖

ReentrantLock 這個類有兩個建構式,一個是無參建構式,一個是傳入 fair 引數的建構式,fair 引數代表的是鎖的公平策略,如果傳入 true 就表示需要構造一個公平鎖,反之則表示要構造一個非公平鎖,

鎖都對應著一個等待佇列,如果一個執行緒沒有獲得鎖,就會進入等待佇列,當有執行緒釋放鎖的時候,就需要從等待佇列中喚醒一個等待的執行緒,如果是公平鎖,喚醒的策略就是誰等待的時間長,就喚醒誰,很公平;如果是非公平鎖,則不提供這個公平保證,有可能等待時間短的執行緒反而先被喚醒,

lock 方法在公平鎖和非公平鎖中的實作:

二者的區別僅在于申請非公平鎖時,如果同步狀態為 0,嘗試將其設為 1,如果成功,直接將當前執行緒置為排它執行緒;否則和公平鎖一樣,呼叫 AQS 獲取獨占鎖方法 acquire

// 非公平鎖實作
final void lock() {
    if (compareAndSetState(0, 1))
    // 如果同步狀態為0,將其設為1,并設定當前執行緒為排它執行緒
        setExclusiveOwnerThread(Thread.currentThread());
    else
    // 呼叫 AQS 獲取獨占鎖方法 acquire
        acquire(1);
}

// 公平鎖實作
final void lock() {
    // 呼叫 AQS 獲取獨占鎖方法 acquire
    acquire(1);
}

4. ReentrantReadWriteLock

ReadWriteLock 適用于讀多寫少的場景,

ReentrantReadWriteLock 類是 ReadWriteLock 介面的具體實作,它是一個可重入的讀寫鎖,ReentrantReadWriteLock 維護了一對讀寫鎖,將讀寫鎖分開,有利于提高并發效率,

讀寫鎖,并不是 Java 語言特有的,而是一個廣為使用的通用技術,所有的讀寫鎖都遵守以下三潭訓本原則:

  • 允許多個執行緒同時讀共享變數;
  • 只允許一個執行緒寫共享變數;
  • 如果一個寫執行緒正在執行寫操作,此時禁止讀執行緒讀共享變數,

讀寫鎖與互斥鎖的一個重要區別就是讀寫鎖允許多個執行緒同時讀共享變數,而互斥鎖是不允許的,這是讀寫鎖在讀多寫少場景下性能優于互斥鎖的關鍵,但讀寫鎖的寫操作是互斥的,當一個執行緒在寫共享變數的時候,是不允許其他執行緒執行寫操作和讀操作,

4.1. ReentrantReadWriteLock 的特性

ReentrantReadWriteLock 的特性如下:

  • ReentrantReadWriteLock 適用于讀多寫少的場景,如果是寫多讀少的場景,由于 ReentrantReadWriteLock 其內部實作比 ReentrantLock 復雜,性能可能反而要差一些,如果存在這樣的問題,需要具體問題具體分析,由于 ReentrantReadWriteLock 的讀寫鎖(ReadLockWriteLock)都實作了 Lock 介面,所以要替換為 ReentrantLock 也較為容易,
  • ReentrantReadWriteLock 實作了 ReadWriteLock 介面,支持了 ReentrantLock 所不具備的讀寫鎖分離,ReentrantReadWriteLock 維護了一對讀寫鎖(ReadLockWriteLock),將讀寫鎖分開,有利于提高并發效率,ReentrantReadWriteLock 的加鎖策略是:允許多個讀操作并發執行,但每次只允許一個寫操作,
  • ReentrantReadWriteLock 為讀寫鎖都提供了可重入的加鎖語意,
  • ReentrantReadWriteLock 支持公平鎖和非公平鎖(默認)兩種模式,

ReadWriteLock 介面定義如下:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}
  • readLock - 回傳用于讀操作的鎖(ReadLock),
  • writeLock - 回傳用于寫操作的鎖(WriteLock),

在讀寫鎖和寫入鎖之間的互動可以采用多種實作方式,ReadWriteLock 的一些可選實作包括:

  • 釋放優先 - 當一個寫入操作釋放寫鎖,并且佇列中同時存在讀執行緒和寫執行緒,那么應該優先選擇讀執行緒、寫執行緒,還是最先發出請求的執行緒?
  • 讀執行緒插隊 - 如果鎖是由讀執行緒持有,但有寫執行緒正在等待,那么新到達的讀執行緒能否立即獲得訪問權,還是應該在寫執行緒后面等待?如果允許讀執行緒插隊到寫執行緒之前,那么將提高并發性,但可能造成執行緒饑餓問題,
  • 重入性 - 讀鎖和寫鎖是否是可重入的?
  • 降級 - 如果一個執行緒持有寫入鎖,那么它能否在不釋放該鎖的情況下獲得讀鎖?這可能會使得寫鎖被降級為讀鎖,同時不允許其他寫執行緒修改被保護的資源,
  • 升級 - 讀鎖能否優先于其他正在等待的讀執行緒和寫執行緒而升級為一個寫鎖?在大多數的讀寫鎖實作中并不支持升級,因為如果沒有顯式的升級操作,那么很容易造成死鎖,

4.2. ReentrantReadWriteLock 的用法

前文了解了 ReentrantReadWriteLock 的特性,接下來,我們要講述其具體用法,

ReentrantReadWriteLock 的構造方法

ReentrantReadWriteLock 和 ReentrantLock 一樣,也有兩個構造方法,且用法相似,

public ReentrantReadWriteLock() {}
public ReentrantReadWriteLock(boolean fair) {}
  • ReentrantReadWriteLock() - 默認構造方法會初始化一個非公平鎖(NonfairSync),在非公平的鎖中,執行緒獲得鎖的順序是不確定的,寫執行緒降級為讀執行緒是可以的,但讀執行緒升級為寫執行緒是不可以的(這樣會導致死鎖),
  • ReentrantReadWriteLock(boolean) - new ReentrantLock(true) 會初始化一個公平鎖(FairSync),對于公平鎖,等待時間最長的執行緒將優先獲得鎖,如果這個鎖是讀執行緒持有,則另一個執行緒請求寫鎖,那么其他讀執行緒都不能獲得讀鎖,直到寫執行緒釋放寫鎖,

ReentrantReadWriteLock 的使用實體

在 ReentrantReadWriteLock 的特性 中已經介紹過,ReentrantReadWriteLock 的讀寫鎖(ReadLockWriteLock)都實作了 Lock 介面,所以其各自獨立的使用方式與 ReentrantLock 一樣,這里不再贅述,

ReentrantReadWriteLock 與 ReentrantLock 用法上的差異,主要在于讀寫鎖的配合使用,本文以一個典型使用場景來進行講解,

【示例】基于 ReadWriteLock 實作一個簡單的泛型無界快取

/**
 * 簡單的無界快取實作
 * <p>
 * 使用 WeakHashMap 存盤鍵值對,WeakHashMap 中存盤的物件是弱參考,JVM GC 時會自動清除沒有被參考的弱參考物件,
 */
static class UnboundedCache<K, V> {

    private final Map<K, V> cacheMap = new WeakHashMap<>();

    private final ReadWriteLock cacheLock = new ReentrantReadWriteLock();

    public V get(K key) {
        cacheLock.readLock().lock();
        V value;
        try {
            value = https://www.cnblogs.com/hehe199316/p/cacheMap.get(key);
            String log = String.format("%s 讀資料 %s:%s", Thread.currentThread().getName(), key, value);
            System.out.println(log);
        } finally {
            cacheLock.readLock().unlock();
        }
        return value;
    }

    public V put(K key, V value) {
        cacheLock.writeLock().lock();
        try {
            cacheMap.put(key, value);
            String log = String.format("%s 寫入資料 %s:%s", Thread.currentThread().getName(), key, value);
            System.out.println(log);
        } finally {
            cacheLock.writeLock().unlock();
        }
        return value;
    }

    public V remove(K key) {
        cacheLock.writeLock().lock();
        try {
            return cacheMap.remove(key);
        } finally {
            cacheLock.writeLock().unlock();
        }
    }

    public void clear() {
        cacheLock.writeLock().lock();
        try {
            this.cacheMap.clear();
        } finally {
            cacheLock.writeLock().unlock();
        }
    }

}

說明:

  • 使用 WeakHashMap 而不是 HashMap 來存盤鍵值對,WeakHashMap 中存盤的物件是弱參考,JVM GC 時會自動清除沒有被參考的弱參考物件,
  • 向 Map 寫資料前加寫鎖,寫完后,釋放寫鎖,
  • 向 Map 讀資料前加讀鎖,讀完后,釋放讀鎖,

測驗其執行緒安全性:

/**
 * @author <a href="mailto:[email protected]">Zhang Peng</a>
 * @since 2020-01-01
 */
public class ReentrantReadWriteLockDemo {

    static UnboundedCache<Integer, Integer> cache = new UnboundedCache<>();

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 20; i++) {
            executorService.execute(new MyThread());
            cache.get(0);
        }
        executorService.shutdown();
    }

    /** 執行緒任務每次向快取中寫入 3 個隨機值,key 固定 */
    static class MyThread implements Runnable {

        @Override
        public void run() {
            Random random = new Random();
            for (int i = 0; i < 3; i++) {
                cache.put(i, random.nextInt(100));
            }
        }

    }

}

說明:示例中,通過執行緒池啟動 20 個并發任務,任務每次向快取中寫入 3 個隨機值,key 固定;然后主執行緒每次固定讀取快取中第一個 key 的值,

輸出結果:

main 讀資料 0:null
pool-1-thread-1 寫入資料 0:16
pool-1-thread-1 寫入資料 1:58
pool-1-thread-1 寫入資料 2:50
main 讀資料 0:16
pool-1-thread-1 寫入資料 0:85
pool-1-thread-1 寫入資料 1:76
pool-1-thread-1 寫入資料 2:46
pool-1-thread-2 寫入資料 0:21
pool-1-thread-2 寫入資料 1:41
pool-1-thread-2 寫入資料 2:63
main 讀資料 0:21
main 讀資料 0:21
// ...

4.3. ReentrantReadWriteLock 的原理

前面了解了 ReentrantLock 的原理,理解 ReentrantReadWriteLock 就容易多了,

ReentrantReadWriteLock 的資料結構

閱讀 ReentrantReadWriteLock 的原始碼,可以發現它有三個核心欄位:

/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
  • sync - 內部類 ReentrantReadWriteLock.Sync 物件,與 ReentrantLock 類似,它有兩個子類:ReentrantReadWriteLock.FairSync 和 ReentrantReadWriteLock.NonfairSync ,分別表示公平鎖和非公平鎖的實作,
  • readerLock - 內部類 ReentrantReadWriteLock.ReadLock 物件,這是一把讀鎖,
  • writerLock - 內部類 ReentrantReadWriteLock.WriteLock 物件,這是一把寫鎖,

ReentrantReadWriteLock 的獲取鎖和釋放鎖

public static class ReadLock implements Lock, java.io.Serializable {

    // 呼叫 AQS 獲取共享鎖方法
    public void lock() {
        sync.acquireShared(1);
    }

    // 呼叫 AQS 釋放共享鎖方法
    public void unlock() {
        sync.releaseShared(1);
    }
}

public static class WriteLock implements Lock, java.io.Serializable {

    // 呼叫 AQS 獲取獨占鎖方法
    public void lock() {
        sync.acquire(1);
    }

    // 呼叫 AQS 釋放獨占鎖方法
    public void unlock() {
        sync.release(1);
    }
}

5. StampedLock

ReadWriteLock 支持兩種模式:一種是讀鎖,一種是寫鎖,而 StampedLock 支持三種模式,分別是:寫鎖、悲觀讀鎖和樂觀讀,其中,寫鎖、悲觀讀鎖的語意和 ReadWriteLock 的寫鎖、讀鎖的語意非常類似,允許多個執行緒同時獲取悲觀讀鎖,但是只允許一個執行緒獲取寫鎖,寫鎖和悲觀讀鎖是互斥的,不同的是:StampedLock 里的寫鎖和悲觀讀鎖加鎖成功之后,都會回傳一個 stamp;然后解鎖的時候,需要傳入這個 stamp,

注意這里,用的是“樂觀讀”這個詞,而不是“樂觀讀鎖”,是要提醒你,樂觀讀這個操作是無鎖的,所以相比較 ReadWriteLock 的讀鎖,樂觀讀的性能更好一些,

StampedLock 的性能之所以比 ReadWriteLock 還要好,其關鍵是 StampedLock 支持樂觀讀的方式,

  • ReadWriteLock 支持多個執行緒同時讀,但是當多個執行緒同時讀的時候,所有的寫操作會被阻塞;
  • 而 StampedLock 提供的樂觀讀,是允許一個執行緒獲取寫鎖的,也就是說不是所有的寫操作都被阻塞,

對于讀多寫少的場景 StampedLock 性能很好,簡單的應用場景基本上可以替代 ReadWriteLock,但是StampedLock 的功能僅僅是 ReadWriteLock 的子集,在使用的時候,還是有幾個地方需要注意一下,

  • StampedLock 不支持重入
  • StampedLock 的悲觀讀鎖、寫鎖都不支持條件變數,
  • 如果執行緒阻塞在 StampedLock 的 readLock() 或者 writeLock() 上時,此時呼叫該阻塞執行緒的 interrupt() 方法,會導致 CPU 飆升,使用 StampedLock 一定不要呼叫中斷操作,如果需要支持中斷功能,一定使用可中斷的悲觀讀鎖 readLockInterruptibly() 和寫鎖 writeLockInterruptibly(),

【示例】StampedLock 阻塞時,呼叫 interrupt() 導致 CPU 飆升

final StampedLock lock
  = new StampedLock();
Thread T1 = new Thread(()->{
  // 獲取寫鎖
  lock.writeLock();
  // 永遠阻塞在此處,不釋放寫鎖
  LockSupport.park();
});
T1.start();
// 保證 T1 獲取寫鎖
Thread.sleep(100);
Thread T2 = new Thread(()->
  // 阻塞在悲觀讀鎖
  lock.readLock()
);
T2.start();
// 保證 T2 阻塞在讀鎖
Thread.sleep(100);
// 中斷執行緒 T2
// 會導致執行緒 T2 所在 CPU 飆升
T2.interrupt();
T2.join();

【示例】StampedLock 讀模板:

final StampedLock sl =
  new StampedLock();

// 樂觀讀
long stamp =
  sl.tryOptimisticRead();
// 讀入方法區域變數
......
// 校驗 stamp
if (!sl.validate(stamp)){
  // 升級為悲觀讀鎖
  stamp = sl.readLock();
  try {
    // 讀入方法區域變數
    .....
  } finally {
    // 釋放悲觀讀鎖
    sl.unlockRead(stamp);
  }
}
// 使用方法區域變數執行業務操作
......

【示例】StampedLock 寫模板:

long stamp = sl.writeLock();
try {
  // 寫共享變數
  ......
} finally {
  sl.unlockWrite(stamp);
}

6. AQS

AbstractQueuedSynchronizer(簡稱 AQS)是佇列同步器,顧名思義,其主要作用是處理同步,它是并發鎖和很多同步工具類的實作基石(如 ReentrantLockReentrantReadWriteLockCountDownLatchSemaphoreFutureTask 等),

6.1. AQS 的要點

AQS 提供了對獨享鎖與共享鎖的支持,

在 java.util.concurrent.locks 包中的相關鎖(常用的有 ReentrantLock、 ReadWriteLock)都是基于 AQS 來實作,這些鎖都沒有直接繼承 AQS,而是定義了一個 Sync 類去繼承 AQS,為什么要這樣呢?因為鎖面向的是使用用戶,而同步器面向的則是執行緒控制,那么在鎖的實作中聚合同步器而不是直接繼承 AQS 就可以很好的隔離二者所關注的事情,

6.2. AQS 的應用

AQS 提供了對獨享鎖與共享鎖的支持,

獨享鎖 API

獲取、釋放獨享鎖的主要 API 如下:

public final void acquire(int arg)
public final void acquireInterruptibly(int arg)
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
public final boolean release(int arg)
  • acquire - 獲取獨占鎖,
  • acquireInterruptibly - 獲取可中斷的獨占鎖,
  • tryAcquireNanos - 嘗試在指定時間內獲取可中斷的獨占鎖,在以下三種情況下回回傳:
    • 在超時時間內,當前執行緒成功獲取了鎖;
    • 當前執行緒在超時時間內被中斷;
    • 超時時間結束,仍未獲得鎖回傳 false,
  • release - 釋放獨占鎖,

共享鎖 API

獲取、釋放共享鎖的主要 API 如下:

public final void acquireShared(int arg)
public final void acquireSharedInterruptibly(int arg)
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
public final boolean releaseShared(int arg)
  • acquireShared - 獲取共享鎖,
  • acquireSharedInterruptibly - 獲取可中斷的共享鎖,
  • tryAcquireSharedNanos - 嘗試在指定時間內獲取可中斷的共享鎖,
  • release - 釋放共享鎖,

6.3. AQS 的原理

ASQ 原理要點:

  • AQS 使用一個整型的 volatile 變數來 維護同步狀態,狀態的意義由子類賦予,
  • AQS 維護了一個 FIFO 的雙鏈表,用來存盤獲取鎖失敗的執行緒,

AQS 圍繞同步狀態提供兩種基本操作“獲取”和“釋放”,并提供一系列判斷和處理方法,簡單說幾點:

  • state 是獨占的,還是共享的;
  • state 被獲取后,其他執行緒需要等待;
  • state 被釋放后,喚醒等待執行緒;
  • 執行緒等不及時,如何退出等待,

至于執行緒是否可以獲得 state,如何釋放 state,就不是 AQS 關心的了,要由子類具體實作,

AQS 的資料結構

閱讀 AQS 的原始碼,可以發現:AQS 繼承自 AbstractOwnableSynchronize

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** 等待佇列的隊頭,懶加載,只能通過 setHead 方法修改, */
    private transient volatile Node head;
    /** 等待佇列的隊尾,懶加載,只能通過 enq 方法添加新的等待節點,*/
    private transient volatile Node tail;
    /** 同步狀態 */
    private volatile int state;
}
  • state - AQS 使用一個整型的 volatile 變數來 維護同步狀態,
    • 這個整數狀態的意義由子類來賦予,如ReentrantLock 中該狀態值表示所有者執行緒已經重復獲取該鎖的次數,Semaphore 中該狀態值表示剩余的許可數量,
  • head 和 tail - AQS 維護了一個 Node 型別(AQS 的內部類)的雙鏈表來完成同步狀態的管理,這個雙鏈表是一個雙向的 FIFO 佇列,通過 head 和 tail 指標進行訪問,當 有執行緒獲取鎖失敗后,就被添加到佇列末尾,

img

再來看一下 Node 的原始碼

static final class Node {
    /** 該等待同步的節點處于共享模式 */
    static final Node SHARED = new Node();
    /** 該等待同步的節點處于獨占模式 */
    static final Node EXCLUSIVE = null;

    /** 執行緒等待狀態,狀態值有: 0、1、-1、-2、-3 */
    volatile int waitStatus;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    /** 前驅節點 */
    volatile Node prev;
    /** 后繼節點 */
    volatile Node next;
    /** 等待鎖的執行緒 */
    volatile Thread thread;

  	/** 和節點是否共享有關 */
    Node nextWaiter;
}

很顯然,Node 是一個雙鏈表結構,

  • waitStatus - Node 使用一個整型的 volatile 變數來 維護 AQS 同步佇列中執行緒節點的狀態,waitStatus 有五個狀態值:
    • CANCELLED(1) - 此狀態表示:該節點的執行緒可能由于超時或被中斷而 處于被取消(作廢)狀態,一旦處于這個狀態,表示這個節點應該從等待佇列中移除,
    • SIGNAL(-1) - 此狀態表示:后繼節點會被掛起,因此在當前節點釋放鎖或被取消之后,必須喚醒(unparking)其后繼結點,
    • CONDITION(-2) - 此狀態表示:該節點的執行緒 處于等待條件狀態,不會被當作是同步佇列上的節點,直到被喚醒(signal),設定其值為 0,再重新進入阻塞狀態,
    • PROPAGATE(-3) - 此狀態表示:下一個 acquireShared 應無條件傳播,
    • 0 - 非以上狀態,

獨占鎖的獲取和釋放

獲取獨占鎖

AQS 中使用 acquire(int arg) 方法獲取獨占鎖,其大致流程如下:

  1. 先嘗試獲取同步狀態,如果獲取同步狀態成功,則結束方法,直接回傳,
  2. 如果獲取同步狀態不成功,AQS 會不斷嘗試利用 CAS 操作將當前執行緒插入等待同步佇列的隊尾,直到成功為止,
  3. 接著,不斷嘗試為等待佇列中的執行緒節點獲取獨占鎖,

img

img

詳細流程可以用下圖來表示,請結合原始碼來理解(一圖勝千言):

img

釋放獨占鎖

AQS 中使用 release(int arg) 方法釋放獨占鎖,其大致流程如下:

  1. 先嘗試獲取解鎖執行緒的同步狀態,如果獲取同步狀態不成功,則結束方法,直接回傳,
  2. 如果獲取同步狀態成功,AQS 會嘗試喚醒當前執行緒節點的后繼節點,
獲取可中斷的獨占鎖

AQS 中使用 acquireInterruptibly(int arg) 方法獲取可中斷的獨占鎖,

acquireInterruptibly(int arg) 實作方式相較于獲取獨占鎖方法( acquire)非常相似,區別僅在于它會通過 Thread.interrupted 檢測當前執行緒是否被中斷,如果是,則立即拋出中斷例外(InterruptedException),

獲取超時等待式的獨占鎖

AQS 中使用 tryAcquireNanos(int arg) 方法獲取超時等待的獨占鎖,

doAcquireNanos 的實作方式 相較于獲取獨占鎖方法( acquire)非常相似,區別在于它會根據超時時間和當前時間計算出截止時間,在獲取鎖的流程中,會不斷判斷是否超時,如果超時,直接回傳 false;如果沒超時,則用 LockSupport.parkNanos 來阻塞當前執行緒,

共享鎖的獲取和釋放

獲取共享鎖

AQS 中使用 acquireShared(int arg) 方法獲取共享鎖,

acquireShared 方法和 acquire 方法的邏輯很相似,區別僅在于自旋的條件以及節點出隊的操作有所不同,

成功獲得共享鎖的條件如下:

  • tryAcquireShared(arg) 回傳值大于等于 0 (這意味著共享鎖的 permit 還沒有用完),
  • 當前節點的前驅節點是頭結點,
釋放共享鎖

AQS 中使用 releaseShared(int arg) 方法釋放共享鎖,

releaseShared 首先會嘗試釋放同步狀態,如果成功,則解鎖一個或多個后繼執行緒節點,釋放共享鎖和釋放獨享鎖流程大體相似,區別在于:

對于獨享模式,如果需要 SIGNAL,釋放僅相當于呼叫頭節點的 unparkSuccessor

獲取可中斷的共享鎖

AQS 中使用 acquireSharedInterruptibly(int arg) 方法獲取可中斷的共享鎖,

acquireSharedInterruptibly 方法與 acquireInterruptibly 幾乎一致,不再贅述,

獲取超時等待式的共享鎖

AQS 中使用 tryAcquireSharedNanos(int arg) 方法獲取超時等待式的共享鎖,

tryAcquireSharedNanos 方法與 tryAcquireNanos 幾乎一致,不再贅述,

7. 死鎖

7.1. 什么是死鎖

死鎖是一種特定的程式狀態,在物體之間,由于回圈依賴導致彼此一直處于等待之中,沒有任何個體可以繼續前進,死鎖不僅僅是在執行緒之間會發生,存在資源獨占的行程之間同樣也 可能出現死鎖,通常來說,我們大多是聚焦在多執行緒場景中的死鎖,指兩個或多個執行緒之間,由于互相持有對方需要的鎖,而永久處于阻塞的狀態,

7.2. 如何定位死鎖

定位死鎖最常見的方式就是利用 jstack 等工具獲取執行緒堆疊,然后定位互相之間的依賴關系,進而找到死鎖,如果是比較明顯的死鎖,往往 jstack 等就能直接定位,類似 JConsole 甚至可以在圖形界面進行有限的死鎖檢測,

如果我們是開發自己的管理工具,需要用更加程式化的方式掃描服務行程、定位死鎖,可以考慮使用 Java 提供的標準管理 API,ThreadMXBean,其直接就提供了 findDeadlockedThreads() 方法用于定位,

7.3. 如何避免死鎖

基本上死鎖的發生是因為:

  • 互斥,類似 Java 中 Monitor 都是獨占的,
  • 長期保持互斥,在使用結束之前,不會釋放,也不能被其他執行緒搶占,
  • 回圈依賴,多個個體之間出現了鎖的回圈依賴,彼此依賴上一環釋放鎖,

由此,我們可以分析出避免死鎖的思路和方法,

(1)避免一個執行緒同時獲取多個鎖,

避免一個執行緒在鎖內同時占用多個資源,盡量保證每個鎖只占用一個資源,

嘗試使用定時鎖 lock.tryLock(timeout),避免鎖一直不能釋放,

對于資料庫鎖,加鎖和解鎖必須在一個資料庫連接中里,否則會出現解鎖失敗的情況,

有完整的Java初級,高級對應的學習路線和資料!專注于java開發,分享java基礎、原理性知識、JavaWeb實戰、spring全家桶、設計模式、分布式及面試資料、開源專案,助力開發者成長!


歡迎關注微信公眾號:碼邦主

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/209837.html

標籤:Java

上一篇:Flink的sink實戰之三:cassandra3

下一篇:Java變數型別Java變數型別

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more