1. 程式人生 > >Java併發程式設計系列之十六:Lock鎖

Java併發程式設計系列之十六:Lock鎖

Lock鎖簡介

Lock鎖機制是JDK 5之後新增的鎖機制,不同於內建鎖,Lock鎖必須顯式宣告,並在合適的位置釋放鎖。Lock是一個介面,其由三個具體的實現:ReentrantLock、ReetrantReadWriteLock.ReadLock 和 ReetrantReadWriteLock.WriteLock,即重入鎖、讀鎖和寫鎖。增加Lock機制主要是因為內建鎖存在一些功能上侷限性。比如無法中斷一個正在等待獲取鎖的執行緒,無法在等待一個鎖的時候無限等待下去。內建鎖必須在釋放鎖的程式碼塊中釋放,雖然簡化了鎖的使用,但是卻造成了其他等待獲取鎖的執行緒必須依靠阻塞等待的方式獲取鎖,也就是說內建鎖實際上是一種阻塞鎖。而新增的Lock鎖機制則是一種非阻塞鎖(這點後面還會詳細介紹)。

首先我們看看Lock介面的原始碼:

public interface Lock {
    //無條件獲取鎖
    void lock();
    //獲取可響應中斷的鎖
    //在獲取鎖的時候可響應中斷,中斷的時候會丟擲中斷異常
    void lockInterruptibly() throws InterruptedException;
    //輪詢鎖。如果不能獲得鎖,則採用輪詢的方式不斷嘗試獲得鎖
    boolean tryLock();
    //定時鎖。如果不能獲得鎖,則每隔unit的時間就會嘗試重新獲取鎖
    boolean tryLock(long time, TimeUnit unit) throws
InterruptedException; //釋放獲得鎖 void unlock(); //獲取繫結的Lock例項的條件變數。在等待某個條件變數滿足的之 //前,lock例項必須被當前執行緒持有。呼叫Condition的await方法 //會自動釋放當前執行緒持有的鎖 Condition newCondition();

註釋寫得很詳細就不再贅述,可以看出Lock鎖機制新增的可響應中斷鎖和使用公平鎖是內建鎖機制鎖沒有的。使用Lock鎖的示例程式碼如下:

Lock lock = new ReentrantLock();
        lock.lock();
        try
{ //更新物件狀態 //如果有異常則捕獲異常 //必要時恢復不變性條件 //如果由return語句必須放在這裡 }finally { lock.unlock(); }

ReentrantLock與synchronized實現策略的比較

前面的文章有提到synchronized使用的是互斥鎖機制,這種同步機制的最大問題在於當由多個執行緒需要獲取通一把鎖的時候只能通過阻塞同步的方式等待已經獲得鎖的執行緒自動釋放鎖。這個過程涉及執行緒的阻塞和執行緒的喚醒,這個過程需要在作業系統從使用者態切換到核心態完成。那麼問題來了,多個執行緒競爭同一把鎖的時候,會引起CPU頻繁的上下文切換,效率很低,系統開銷也很大。這種策略被稱為悲觀併發策略,也是synchronized使用的併發策略。

ReentrantLock使用了更為先進的併發策略,既然互斥同步造成的阻塞會影響系統的效能,有沒有一種辦法不用阻塞也能實現同步呢?併發大師Doug Lea(也是Lock鎖的作者)提出了以自旋的方式獲得鎖。簡單來說,如果需要獲得鎖不存在爭用的情況,那麼獲取成功;如果鎖存在爭用的情況,那麼使用失敗補償措施(jdk 5之後到目前的jdk 8使用的是不斷嘗試重新獲取,直到獲取成功)解決爭用的矛盾。由於自旋發生線上程內部,所以不用阻塞其他的執行緒,也就是實現了非阻塞同步。這種策略也稱為基於衝突檢測的樂觀併發策略,也是ReentrantLock使用的併發策略。

簡單總結ReentrantLock和synchronized,前者的先進性體現在以下幾點:

  1. 可響應中斷的鎖。當在等待鎖的執行緒如果長期得不到鎖,那麼可以選擇不繼續等待而去處理其他事情,而synchronized的互斥鎖則必須阻塞等待,不能被中斷
  2. 可實現公平鎖。所謂公平鎖指的是多個執行緒在等待鎖的時候必須按照執行緒申請鎖的時間排隊等待,而非公平性鎖則保證這點,每個執行緒都有獲得鎖的機會。synchronized的鎖和ReentrantLock使用的預設鎖都是非公平性鎖,但是ReentrantLock支援公平性的鎖,在建構函式中傳入一個boolean變數指定為true實現的就是公平性鎖。不過一般而言,使用非公平性鎖的效能優於使用公平性鎖
  3. 每個synchronized只能支援繫結一個條件變數,這裡的條件變數是指執行緒執行等待或者通知的條件,而ReentrantLock支援繫結多個條件變數,通過呼叫lock.newCondition()可獲取多個條件變數。不過使用多少個條件變數需要依據具體情況確定。

如何在ReentrantLock和synchronized之間進行選擇

在一些內建鎖無法滿足一些高階功能的時候才考慮使用ReentrantLock。這些高階功能包括:可定時的、可輪詢的與可中斷的鎖獲取操作,公平佇列,以及非塊結構的鎖。否則還是應該優先使用synchronized。

這段話是併發大師Brian Goetz的建議。那麼,我們來分析一下,為什麼在ReentrantLock具有那麼多優勢的前提下仍然建議優先使用synchronized呢?

首先,內建鎖被開發人員鎖熟悉(這個理由當然不足以讓人信服),而且內建鎖的優勢在於避免了手動釋放鎖這一操作。如果在使用ReentrantLock的時候忘記在finally呼叫unlock了,那麼就相當於埋下了一顆定時炸彈,並且影響其他程式碼的執行(還不夠有說服力)。其次,使用內建鎖dump執行緒資訊可以幫助分析哪些呼叫幀獲得了哪些鎖,並且能夠幫助檢測和識別發生死鎖的執行緒。這點是ReentrantLock無法做到的(有那麼一點說服力了)。最後,synchronized未來還將繼續優化,目前的synchronized已經進行了自適應、自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等方面的優化,線上程阻塞和執行緒喚醒方面的效能已經沒有那麼大了。另一方面,ReentrantLock的效能可能就止步於此,未來優化的可能性很小(好吧,我認了)。這點主要是由於synchronized是JVM的內建屬性,執行synchronized優化自然順理成章(嘿嘿,畢竟是親兒子嘛)。

使用可中斷鎖

可中斷鎖的使用示例如下:

    ReentrantLock lock = new ReentrantLock();
    ...........
    lock.lockInterruptibly();//獲取響應中斷鎖
    try {
        //更新物件的狀態
        //捕獲異常,必要時恢復到原來的不變性條件
        //如果有return語句必須放在這裡,原因已經說過了
    }finally{
        lock.unlock();
        //鎖必須在finally塊中釋放
    }

下面通過一個具體的例子演示如何使用可中斷鎖:

首先我們看看使用synchronized同步然後嘗試進行中斷的例子

package com.rhwayfun.concurrency.r0405;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * Created by rhwayfun on 16-4-5.
 */
public class SyncInterruptDemo {

    //鎖物件
    private static Object lock = new Object();
    //日期格式器
    private static DateFormat format = new SimpleDateFormat("HH:mm:ss");

    /**
     * 寫資料
     */
    public void write(){
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + ":start writing data at " + format.format(new Date()));
            long start = System.currentTimeMillis();
            for (;;){
                //寫15秒的資料
                if (System.currentTimeMillis() - start > 1000 * 15){
                    break;
                }
            }
            //過了15秒才會執行到這裡
            System.out.println(Thread.currentThread().getName() + ":finish writing data at " + format.format(new Date()));
        }
    }

    /**
     * 讀資料
     */
    public void read(){
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + ":start reading data at "
                    + format.format(new Date()));
        }
    }

    /**
     * 執行寫資料的執行緒
     */
    static class Writer implements Runnable{

        private SyncInterruptDemo syncInterruptDemo;

        public Writer(SyncInterruptDemo syncInterruptDemo) {
            this.syncInterruptDemo = syncInterruptDemo;
        }

        public void run() {
            syncInterruptDemo.write();
        }
    }

    /**
     * 執行讀資料的執行緒
     */
    static class Reader implements Runnable{

        private SyncInterruptDemo syncInterruptDemo;

        public Reader(SyncInterruptDemo syncInterruptDemo) {
            this.syncInterruptDemo = syncInterruptDemo;
        }

        public void run() {
            syncInterruptDemo.read();
            System.out.println(Thread.currentThread().getName() + ":finish reading data at "
                    + format.format(new Date()));
        }
    }

    public static void main(String[] args) throws InterruptedException {

        SyncInterruptDemo syncInterruptDemo = new SyncInterruptDemo();

        Thread writer = new Thread(new Writer(syncInterruptDemo),"Writer");
        Thread reader = new Thread(new Reader(syncInterruptDemo),"Reader");

        writer.start();
        reader.start();

        //執行5秒,然後嘗試中斷讀執行緒
        TimeUnit.SECONDS.sleep(5);
        System.out.println(reader.getName() +":I don't want to wait anymore at " + format.format(new Date()));
        //中斷讀的執行緒
        reader.interrupt();
    }
}

執行結果如下:

這裡寫圖片描述

從結果可以看到,嘗試在讀執行緒執行5秒後中斷它,發現無果,因為寫執行緒需要執行15秒,sleep5秒後過了10秒(sleep的5秒加上10剛好是寫執行緒的15秒)讀執行緒才顯示中斷的資訊,意味著在寫執行緒釋放鎖之後才響應了主執行緒的中斷事件,也就是說在synchronized程式碼塊執行期間不允許被中斷,這點也驗證了上面對synchronized的討論。

然後我們使用ReentrantLock試一下,讀執行緒能否正常響應中斷,根據分析,在讀執行緒執行5秒後,主執行緒中斷讀執行緒的時候讀執行緒應該能夠正常響應中斷,然後停止執行讀資料的操作。我們看看程式碼:

package com.rhwayfun.concurrency.r0405;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by rhwayfun on 16-4-5.
 */
public class LockInterruptDemo {
    //鎖物件
    private static Lock lock = new ReentrantLock();
    //日期格式器
    private static DateFormat format = new SimpleDateFormat("HH:mm:ss");

    /**
     * 寫資料
     */
    public void write() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + ":start writing data at "
                    + format.format(new Date()));
            long start = System.currentTimeMillis();
            for (;;){
                if (System.currentTimeMillis() - start > 1000 * 15){
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName() + ":finish writing data at "
                    + format.format(new Date()));
        }finally {
            lock.unlock();
        }
    }

    /**
     * 讀資料
     */
    public void read() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            System.out.println(Thread.currentThread().getName() + ":start reading data at "
                    + format.format(new Date()));
        }finally {
            lock.unlock();
        }
    }

    /**
     * 執行寫資料的執行緒
     */
    static class Writer implements Runnable {

        private LockInterruptDemo lockInterruptDemo;

        public Writer(LockInterruptDemo lockInterruptDemo) {
            this.lockInterruptDemo = lockInterruptDemo;
        }

        public void run() {
            lockInterruptDemo.write();
        }
    }

    /**
     * 執行讀資料的執行緒
     */
    static class Reader implements Runnable {

        private LockInterruptDemo lockInterruptDemo;

        public Reader(LockInterruptDemo lockInterruptDemo) {
            this.lockInterruptDemo = lockInterruptDemo;
        }

        public void run() {
            try {
                lockInterruptDemo.read();
                System.out.println(Thread.currentThread().getName() + ":finish reading data at "
                        + format.format(new Date()));
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + ": interrupt reading data at "
                        + format.format(new Date()));
            }
            System.out.println(Thread.currentThread().getName() + ":end reading data at "
                    + format.format(new Date()));
        }
    }

    public static void main(String[] args) throws InterruptedException {

        LockInterruptDemo lockInterruptDemo = new LockInterruptDemo();

        Thread writer = new Thread(new Writer(lockInterruptDemo), "Writer");
        Thread reader = new Thread(new Reader(lockInterruptDemo), "Reader");

        writer.start();
        reader.start();

        //執行5秒,然後嘗試中斷
        TimeUnit.SECONDS.sleep(5);
        System.out.println(reader.getName() + ":I don't want to wait anymore at " + format.format(new Date()));
        //中斷讀的執行緒
        reader.interrupt();
    }

}

執行結果如下:

這裡寫圖片描述

顯然,讀執行緒正常響應了我們的中斷,因為讀執行緒輸出了中斷資訊,即使寫執行緒寫完資料後,讀執行緒也沒有輸出結束讀資料的資訊,這點是在我們意料之中的。這樣也驗證了可中斷鎖的分析。