Java多執行緒/併發06、執行緒鎖Lock與ReadWriteLock

唐大麥發表於2017-04-28

java的基本鎖型別,都以介面形式出現,常用的有以下兩種鎖的介面:

  • Lock鎖。它的實現有ReentrantLock, ReentrantReadWriteLock.ReadLock,
    ReentrantReadWriteLock.WriteLock
  • ReadWriteLock鎖。它的實現有ReentrantReadWriteLock。

一、lock簡單使用方法

1、Lock鎖基本都是排他鎖,它和synchronized很類似,都能對一塊程式碼進行上鎖,從而使得同一時間內只有一個執行緒能訪問。那麼有什麼差別呢?
Lock 和 synchronized 有一點明顯的區別 —— lock 必須在 finally 塊中釋放。否則,如果受保護的程式碼將丟擲異常,鎖就有可能永遠得不到釋放!這一點區別極為重要。忘記在 finally 塊中釋放鎖,可能會在程式中留下一個定時炸彈,當有一天炸彈爆炸時,您要花費很大力氣才有找到源頭在哪。而使用同步,JVM 將確保鎖會獲得自動釋放。除此之外,當許多執行緒都在爭用同一個鎖時,使用 ReentrantLock 的總體開支卻比 synchronized 少很多。
還記得《synchronized同步 》例子嗎,我們用lock改寫一下:

class Pan1 {
    private Lock lock = new ReentrantLock(); 
    /*烹飪方法,該方法輸出步驟*/
    public void Cook(String[] steps) {
         lock.lock();
         try{
            for (int i = 0; i < steps.length; i++) {
                /*模擬競爭造成的執行緒等待,這樣效果明顯些*/
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(steps[i]);
            }
            System.out.println("");
         }finally{
             /*
             * synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,
             * 而且在程式碼執行出現異常時,JVM會自動釋放鎖定。
             * 但是使用Lock則不行,lock是通過程式碼實現的。
             * 要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中
             */
            lock.unlock();
         }

    }
    /*青椒炒肉製作步驟:a1.放肉,a2.放鹽,a3.放辣椒  a4 a5....*/
    String[] steps_LaJiaoChaoRou={"a1.","a2.","a3.","a4.","a5.","a6.","a7.","a8.","a9.","a10.","OK:辣椒炒肉"};
    /*番茄炒蛋製作步驟:b1.放蛋,b2.放鹽,b3.放番茄*/
    String[] steps_FanQieChaoDan={"b1.","b2.","b3.","b4.","b5.","b6.","OK:番茄炒蛋"};
}

public class LockDemo {
    public static void main(String[] args) {

        final Pan pan=new Pan();
        /*執行緒1:老大炒青椒炒肉。*/
        new Thread(){
            public void run() {
                /*為了看出錯亂效果,這裡用死迴圈,一段時間後手工點選停止執行按鈕*/
                while (true) {
                    try {
                        /*青椒炒肉需要5秒;*/
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pan.Cook(pan.steps_LaJiaoChaoRou);
                }
            }
        }.start();

        /*執行緒2:老二炒番茄炒蛋。*/
        new Thread(){
            public void run() {
                /*為了看出錯亂效果,這裡用死迴圈,一段時間後手工點選停止執行按鈕*/
                while (true) {
                    try {
                        /*番茄炒蛋需要5秒;*/
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pan.Cook(pan.steps_FanQieChaoDan);
                }
            }
        }.start();
    }
}

有人總結了lock和synchronized同步的區別:
1. lock是一個介面,而synchronized是java的一個關鍵字,synchronized是內建的語言實現;(具體實現上的區別在《Java虛擬機器》中有講解底層的CAS不同,以前有讀過現在又遺忘了。)
2. synchronized在發生異常時候會自動釋放佔有的鎖,因此不會出現死鎖;而lock發生異常時候,不會主動釋放佔有的鎖,必須手動unlock來釋放鎖,可能引起死鎖的發生。(所以最好將同步程式碼塊用try catch包起來,finally中寫入unlock,避免死鎖的發生。)
3. lock等待鎖過程中可以用interrupt來終端等待,而synchronized只能等待鎖的釋放,不能響應中斷;
4. lock可以通過trylock來知道有沒有獲取鎖,而synchronized不能;
5. Lock可以提高多個執行緒進行讀操作的效率。(可以通過readwritelock實現讀寫分離)

二、ReadWriteLock讀寫鎖

ReadWriteLock包含兩部分:讀鎖、寫鎖。上讀鎖時,程式碼塊中資料對其它執行緒來說可讀不可寫;上寫鎖時,程式碼塊中的資料對其它執行緒來說不可寫也不可讀。由於實現了讀寫分離,在讀比寫多的場景下,這無疑更高效。
API中的例子很經典:

class CachedData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        /* 初始化變數,需要寫入資料 */
        if (!cacheValid) {
            // 在獲取寫鎖前釋放讀鎖
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                /* 重新檢查狀態,因為其它執行緒很可能在這之前獲取了寫鎖,並改寫了資料 */
                if (!cacheValid) {
                    System.out.println(Thread.currentThread().getName()
                            + ": 快取未初始化");
                    data = getData();
                    cacheValid = true;
                    System.out.println(Thread.currentThread().getName()
                            + ": 快取初始完成,當前值:" + data);
                }
                /* 降級鎖:在釋放寫鎖前獲取讀鎖 */
                rwl.readLock().lock();
            } finally {
                rwl.writeLock().unlock(); /* 釋放寫鎖,依然保持讀鎖 */
            }
        }
        /* 此時,依然存在讀鎖 */
        try {
            System.out.println(Thread.currentThread().getName() + ": 獲取快取值為:"+ data);
        } finally {
            /* 釋放讀鎖 */
            rwl.readLock().unlock();
        }
    }
    int getData(){
        return new Random().nextInt(100000);
    }
}

public class LockDemo {
    public static void main(String[] args) {
        final CachedData cachedData = new CachedData();
        for (int i = 0; i < 5; i++) {
            new Thread() {
                public void run() {
                    cachedData.processCachedData();
                }
            }.start();
        }
    }
}

輸出:

Thread-0: 快取未初始化
Thread-0: 快取未初始完成,當前值:27269
Thread-0: 獲取快取值為:27269
Thread-1: 獲取快取值為:27269
Thread-3: 獲取快取值為:27269
Thread-2: 獲取快取值為:27269
Thread-4: 獲取快取值為:27269

我們把鎖去掉,看看會輸出什麼
改寫CachedData類:

class CachedData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        /* 初始化變數,需要寫入資料 */
        if (!cacheValid) {
            System.out.println(Thread.currentThread().getName()
                    + ": 快取未初始化");
            data = getData();
            cacheValid = true;
            System.out.println(Thread.currentThread().getName()
                    + ": 快取初始完成,當前值:" + data);
        }

        System.out.println(Thread.currentThread().getName() + ": 獲取快取值為:"
                + data);
    }
    int getData(){
        return new Random().nextInt(100000);
    }
}

輸出:

Thread-0: 快取未初始化
Thread-1: 快取未初始化
Thread-2: 快取未初始化
Thread-0: 快取初始完成,當前值:41131
Thread-0: 獲取快取值為:41131
Thread-2: 快取初始完成,當前值:41131
Thread-2: 獲取快取值為:41131
Thread-1: 快取初始完成,當前值:11655
Thread-1: 獲取快取值為:11655
Thread-3: 獲取快取值為:11655
Thread-4: 獲取快取值為:11655

亂套了,一開始3個執行緒同時都去寫快取了,然後獲取快取也各不相同。

總結:

在運用讀寫鎖時,注意鎖的降級:
寫鎖是可以獲得讀鎖的,即:

rwl.writeLock().lock();
//在寫鎖狀態中,可以獲取讀鎖
rwl.readLock().lock();
rwl.writeLock().unlock();

讀鎖是不能夠獲得寫鎖的,如果要加寫鎖,本執行緒必須釋放所持有的讀鎖,即:

rwl.readLock().lock();
//......
//必須釋放掉讀鎖,才能夠加寫鎖
rwl.readLock().unlock();
rwl.writeLock().lock();

相關文章