Java多線程:線程間通訊之Lock

Java 5 以後,Java在內置關鍵字sychronized的基礎上又增長了一個新的處理鎖的方式,Lock類。html

因爲在Java線程間通訊:volatile與sychronized中,咱們已經詳細的瞭解了synchronized,因此咱們如今主要介紹一下Lock,以及將Lock與synchronized進行一下對比。java

1. synchronized的缺陷

synchronized修飾的代碼只有獲取鎖的線程纔可以執行,其餘線程只能等待該線程釋放鎖。一個線程釋放鎖的狀況有如下方式:編程

  • 獲取鎖的線程完成了synchronized修飾的代碼塊的執行。
  • 線程執行時發生異常,JVM自動釋放鎖。

咱們在Java多線程的生命週期,實現與調度中談過,鎖會由於等待I/O,sleep()方法等緣由被阻塞而不釋放鎖,此時若是線程還處於用synchronized修飾的代碼區域裏,那麼其餘線程只能等待,這樣就影響了效率。所以Java提供了Lock來實現另外一個機制,即不讓線程無限期的等待下去。安全

思考一個情景,當多線程讀寫文件時,讀操做和寫操做會發生衝突,寫操做和寫操做會發生衝突,但讀操做和讀操做不會有衝突。若是使用synchronized來修飾的話,就極可能形成多個讀操做沒法同時進行的可能(若是隻用synchronized修飾寫方法,那麼可能形成讀寫衝突,若是同時修飾了讀寫方法,則會有讀讀干擾)。此時就須要用到Lock,換言之Lock比synchronized提供了更多的功能。多線程

使用Lock須要注意如下兩點:併發

  • Lock不是語言內置的,synchronized是Java關鍵字,爲內置特性,Lock是一個類,經過這個類能夠實現同步訪問。
  • 採用synchronized時咱們不須要手動去控制加鎖和釋放,系統會自動控制。而使用Lock類,咱們須要手動的加鎖和釋放,不主動釋放可能會形成死鎖。實際上Lock類的使用某種意義上講要比synchronized更加直觀。

2. Lock類接口設計

Lock類自己是一個接口,其方法以下:ide

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

下面依次講解一下其中各個方法。性能

  • lock() 方法使用最多,做用是用於獲取鎖,若是鎖已經被其餘線程得到,則等待。
    一般狀況下,lock使用如下方式去獲取鎖:
Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){
     
}finally{
    lock.unlock();   //釋放鎖
}
  • lockInterruptibly() 和lock()的區別是lockInterruptibly()鎖定的線程處於等待狀態時,容許線程的打斷操做,線程使用Thread.interrupt()打斷該線程後會直接返回並拋出一個InterruptException();lock()方法鎖定對象時若是在等待時檢測到線程使用Thread.interrupt(),仍然會繼續嘗試獲取鎖,失敗則繼續休眠,只是在成功獲取鎖以後在把當前線程置爲interrupt狀態。也就使說,當兩個線程同時經過lockInterruptibly()想獲取某個鎖時,倘若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法可以中斷線程B的等待過程。
    所以,lockInterruptibly()方法必須實現catch(InterruptException e)代碼塊。常見使用方式以下:
public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}
  • tryLock() 和lock()最大的不一樣是具備返回值,或者說,它不去等待鎖。若是它成功獲取鎖,那麼返回true;若是它沒法成功獲取鎖,則返回false。
    一般狀況下,tryLock使用方式以下:
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //若是不能獲取鎖,則直接作其餘事情
}
  • tryLock(long time, TimeUnit unit) 則是介於兩者之間,用戶設定一個等待時間,若是在這個時間內獲取到了鎖,則返回true,不然返回false結束。
  • unlock() 從上面的代碼裏咱們也看到,unlock()通常放在異常處理操做的finally字符控制的代碼塊中。咱們要記得Lock和sychronized的區別,防止產生死鎖。
  • newCondition() 該方法咱們放到後面講。

3. ReentrantLock可重入鎖

3.1. ReentrantLock概述

ReentrantLock譯爲「可重入鎖」,咱們在Java多線程:synchronized的可重入性中已經明白了什麼是可重入以及理解了synchronized的可重入性。ReentrantLock是惟一實現Lock接口的類。優化

3.2. ReentrantLock使用

考慮到如下情景,一個僅出售雙人票的演唱會進行門票出售,有三個售票口同時進行售票,買票須要100ms時間,每張票出票須要100ms時間。該如何設計這個情景?this

package com.cielo.LockTest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static java.lang.Thread.sleep;

/**
 * Created by 63289 on 2017/4/10.
 */
class SoldTicket implements Runnable {
    Lock lock = new ReentrantLock();//使用可重入鎖
    private volatile Integer ticket;//保證從主內存獲取

    SoldTicket(Integer ticket) {
        this.ticket = ticket;//提供票數
    }

    private void sold() {
        lock.lock();//鎖定操做放在try代碼塊外
        try {
            if (ticket <= 0) return;//當ticket==2時可能有多個線程進入sold方法,一個線程運行後另外兩個線程須要退出。
            sleep(200);//買票0.1s,出票0.1s
            --ticket;
            System.out.println("The first ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave.");//獲取線程id來識別出票站。
            sleep(100);//出票0.1s
            --ticket;
            System.out.println("The second ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
        while (ticket > 0) {
            sold();
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        SoldTicket soldTicket = new SoldTicket(20);
        new Thread(soldTicket).start();
        new Thread(soldTicket).start();
        new Thread(soldTicket).start();
    }
}

上面這段代碼結果以下:

The first ticket is sold by 11, 19 tickets leave.
The second ticket is sold by 11, 18 tickets leave.
The first ticket is sold by 13, 17 tickets leave.
The second ticket is sold by 13, 16 tickets leave.
The first ticket is sold by 13, 15 tickets leave.
The second ticket is sold by 13, 14 tickets leave.
The first ticket is sold by 12, 13 tickets leave.
The second ticket is sold by 12, 12 tickets leave.
The first ticket is sold by 11, 11 tickets leave.
The second ticket is sold by 11, 10 tickets leave.
The first ticket is sold by 11, 9 tickets leave.
The second ticket is sold by 11, 8 tickets leave.
The first ticket is sold by 13, 7 tickets leave.
The second ticket is sold by 13, 6 tickets leave.
The first ticket is sold by 13, 5 tickets leave.
The second ticket is sold by 13, 4 tickets leave.
The first ticket is sold by 13, 3 tickets leave.
The second ticket is sold by 13, 2 tickets leave.
The first ticket is sold by 13, 1 tickets leave.
The second ticket is sold by 13, 0 tickets leave.

若是咱們不對售票操做進行鎖定,則會有如下幾個問題:

  • 出售第一張票後其餘機器出了另外一張票,致使票沒有成對賣。
  • 已經無票後仍有機器出票形成混亂。

顯然,本題的情景用synchronized也能夠很容易的實現,實際上Lock有別於synchronized的主要點是lockInterruptibly()和tryLock()這兩個能夠對鎖進行控制的方法。

4. ReadWriteLock讀寫鎖

4.1. ReadWriteLock接口

回到開頭synchronized缺陷的介紹,實際上,Lock接口的重要衍生接口ReadWriteLock便是解決這一問題。ReadWriteLock定義很簡單,僅有兩個接口:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();
 
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

便是它只提供了readLock()和writeLock()兩個操做,這兩個操做均返回一個Lock類的實例。兩個操做一個獲取讀鎖,一個獲取寫鎖,將讀寫分開進行操做。ReadWriteLock將讀寫的鎖分開,可讓多個讀操做並行,這就大大提升了效率。使用ReadWriteLock時,用讀鎖去控制讀操做,寫鎖控制寫操做,進而實現了一個能夠在以下的大量讀少許寫且讀者優先的情景運行的鎖。

4.2. ReentrantReadWriteLock可重入讀寫鎖

ReentrantReadWriteLock是ReadWriteLock的惟一實例。同時提供了不少操做方法。ReentratReadWriteLock接口實現的讀鎖寫鎖進入有以下要求:

4.2.1. 線程進入讀鎖的要求

  • 沒有其餘線程的寫鎖。
  • 沒有鎖請求 或 調用寫請求的線程正是該線程。

4.2.2. 線程進入寫鎖的要求

  • 沒有其餘線程的讀鎖。
  • 沒有其餘線程的寫鎖。

4.2.3. 讀寫鎖使用示例

private SomeClass someClass;//資源
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//建立鎖
private final Lock readLock = readWriteLock.readLock();//讀鎖
private final Lock writeLock = readWriteLock.writeLock();//寫鎖
//讀方法
readLock.lock();
try {
    result = someClass.someMethod();
} catch (Exception e) {
    e.printStackTrace();
} finally {
    readLock.unlock();
}
//寫方法,產生新的SomeClass實例tempSomeClass  
writeLock.lock();
try{
    this.someClass = tempSomeClass;//更新
}catch (Exception e) {
    e.printStackTrace();
} finally{
    writeLock.unlock();
}

5. 公平鎖

公平鎖即當多個線程等待的一個資源的鎖釋放時,線程不是隨機的獲取資源而是等待時間最久的線程獲取資源(FIFO)。Java中,synchronized是一個非公平鎖,沒法保證鎖的獲取順序。ReentrantLock和ReentrantReadWriteLock默認也是非公平鎖,但能夠設置成公平鎖。咱們前面的實例中初始化ReentrantLock和ReentrantReadWriteLock時都是無參數的。實際上,它們提供一個默認的boolean變量fair,爲true則爲公平鎖,爲false則爲非公平鎖,默認爲false。所以,當咱們想將其實現爲公平鎖時,僅須要初始化時賦值true。即:

Lock lock = new ReentrantLock(true);

考慮前面賣票的實例,若是改成公平鎖(儘管這和情景無關),則結果輸出很是整齊以下:

The first ticket is sold by 11, 19 tickets leave.
The second ticket is sold by 11, 18 tickets leave.
The first ticket is sold by 12, 17 tickets leave.
The second ticket is sold by 12, 16 tickets leave.
The first ticket is sold by 13, 15 tickets leave.
The second ticket is sold by 13, 14 tickets leave.
The first ticket is sold by 11, 13 tickets leave.
The second ticket is sold by 11, 12 tickets leave.
The first ticket is sold by 12, 11 tickets leave.
The second ticket is sold by 12, 10 tickets leave.
The first ticket is sold by 13, 9 tickets leave.
The second ticket is sold by 13, 8 tickets leave.
The first ticket is sold by 11, 7 tickets leave.
The second ticket is sold by 11, 6 tickets leave.
The first ticket is sold by 12, 5 tickets leave.
The second ticket is sold by 12, 4 tickets leave.
The first ticket is sold by 13, 3 tickets leave.
The second ticket is sold by 13, 2 tickets leave.
The first ticket is sold by 11, 1 tickets leave.
The second ticket is sold by 11, 0 tickets leave.

6. Lock和synchronized的選擇

  • synchronized是內置語言實現的關鍵字,Lock是爲了實現更高級鎖功能而提供的接口。
  • Lock實現了tryLock等接口,線程能夠不用一直等待。
  • synchronized發生異常時自動釋放佔有的鎖,Lock須要在finally塊中手動釋放鎖。所以從安全性角度講,既能夠用Lock又能夠用synchronized時(即不須要鎖的更高級功能時)使用synchronized更保險。
  • Lock能夠經過lockInterruptibly()接口實現可中斷鎖。
  • 因爲Lock提供了時間限制同步,可被打斷同步等機制,線程激烈競爭時Lock的性能遠優於synchronized,即有大量線程時推薦使用Lock。在競爭不激烈時,因爲synchronized的編譯器優化更好,性能更佳。
  • ReentrantReadWriteLock實現了封裝好的讀寫鎖用於大量讀少許寫讀者優先情景解決了synchronized讀寫情景難以實現問題。

7. 參考文章

Java併發編程:Lock

lock和lockInterruptibly

說說ReentrantReadWriteLock

相關文章
相關標籤/搜索