天天看點

Java并發Concurrent包的鎖(一)——Lock接口Lock接口synchronized和Lock

Java 并發包 Concurrent 的包結構共可分為五個部分:

- 原子類

- 鎖

- collection并發集合架構

- excutor線程池

- 同步工具

本文介紹 Lock 接口和其與 synchronized 關鍵字的對比。

Lock接口

盡管 synchronized 在文法上已經足夠簡單了,在 JDK5 之前隻能借助此實作,但是由于是獨占鎖,性能卻不高,是以 JDK5 以後就開始借助于 JNI 來完成更進階的鎖實作。

Lock 實作提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。此實作允許更靈活的結構,可以具有差别很大的屬性,可以支援多個相關的 Condition 對象。

Lock 實作提供了使用 synchronized 方法和語句所沒有的其他功能,包括提供了一個非塊結構的擷取鎖嘗試 (tryLock())、一個擷取可中斷鎖的嘗試 (lockInterruptibly()) 和一個擷取逾時失效鎖的嘗試 (tryLock(long, TimeUnit))。

void lock()

擷取鎖。

如果鎖不可用,出于線程排程目的,将禁用目前線程,并且在獲得鎖之前,該線程将一直處于休眠狀态。

void lockInterruptibly()

如果目前線程未被中斷,則擷取鎖。

如果鎖可用,則擷取鎖,并立即傳回。

如果鎖不可用,出于線程排程目的,将禁用目前線程,并且在發生以下兩種情況之一以前,該線程将一直處于休眠狀态:

鎖由目前線程獲得;

或者其他某個線程中斷目前線程,并且支援對鎖擷取的中斷。

boolean tryLock()

僅在調用時鎖為空閑狀态才擷取該鎖。

如果鎖可用,則擷取鎖,并立即傳回值 true。如果鎖不可用,則此方法将立即傳回值 false。

boolean tryLock(long time, TimeUnit unit)

如果鎖在給定的等待時間内空閑,并且目前線程未被 中斷,則擷取鎖。

如果鎖可用,則此方法将立即傳回值 true。如果鎖不可用,出于線程排程目的,将禁用目前線程,并且在發生以下三種情況之一前,該線程将一直處于休眠狀态:

鎖由目前線程獲得;或者

其他某個線程中斷目前線程,并且支援對鎖擷取的中斷;或者

已超過指定的等待時間

如果獲得了鎖,則傳回值 true。

如果目前線程:

在進入此方法時已經設定了該線程的中斷狀态;或者

在擷取鎖時被中斷,并且支援對鎖擷取的中斷,

則将抛出 InterruptedException,并會清除目前線程的已中斷狀态。

如果超過了指定的等待時間,則将傳回值 false。如果 time 小于等于 0,該方法将完全不等待。

void unlock()

釋放鎖。

Lock 實作通常對哪個線程可以釋放鎖施加了限制(通常隻有鎖的保持者可以釋放它),如果違背了這個限制,可能會抛出(未經檢查的)異常。該 Lock 實作必須對所有限制和異常類型進行記錄。

Condition newCondition()

傳回綁定到此 Lock 執行個體的新 Condition 執行個體。

在等待條件前,鎖必須由目前線程保持。調用 Condition.await() 将在等待前以原子方式釋放鎖,并在等待傳回前重新擷取鎖。

synchronized和Lock

可以比較一下 synchronized 關鍵字和 lock 的性能:

(JDK6以後對 synchronized 進行了很多優化,而 Lock 沒有太多優化空間,二者其實開銷是差不多的。在更新的版本中内置鎖隻會比顯式鎖性能更好。)

一個簡單的例子:

public class TestLock {

    private static int a = ;

    private static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        final int threadSize = ;
        Thread[] ts = new Thread[threadSize];
        for (int i = ; i < threadSize; i++) {
            ts[i] = new Thread() {
                public void run() {
                    synchronized (obj) {
                        a++;
                    }
                }
            };
        }
        long start = System.currentTimeMillis();
        for(Thread t:ts) {
            t.start();
        }
        for(Thread t:ts) {
            t.join();
        }
        long end = System.currentTimeMillis();
        System.out.println(a);
        System.out.println(end-start);
    }

}
           

運作結果:

100000
6562
           

改為使用 Lock 接口實作的 ReentrantLock:

public class TestLock2 {

    private static int a = ;

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        final int threadSize = ;
        Thread[] ts = new Thread[threadSize];
        for (int i = ; i < threadSize; i++) {
            ts[i] = new Thread() {
                public void run() {
                    lock.lock();
                    try{
                        a++;
                    }finally{
                        lock.unlock();
                    }
                }
            };
        }
        long start = System.currentTimeMillis();
        for(Thread t:ts) {
            t.start();
        }
        for(Thread t:ts) {
            t.join();
        }
        long end = System.currentTimeMillis();
        System.out.println(a);
        System.out.println(end-start);
    }

}
           

運作結果:

100000
6431
           

根據多次運作的結果,發現在 JDK8 中兩種鎖的開銷其實是差不多的。

其實是由于這兩種鎖都是獨占鎖,JDK5 以前内置鎖性能低的原因是它沒做任何優化,直接使用系統的互斥體來擷取鎖。顯式鎖除了 CAS 的時候利用的是本地代碼以外,其它的部分都是 Java 代碼實作的,在後續版本的 Java 中,顯式鎖不太可能會比内置鎖好,隻會更差。使用顯式鎖的唯一理由是要利用它更多的功能。