(轉載)Java顯式鎖學習總結之一:概論
=========================================================================================
我們都知道在java中,當多個執行緒需要併發訪問共享資源時需要使用同步,我們經常使用的同步方式就是synchronized關鍵字,事實上,在jdk1.5之前,只有synchronized一種同步方式。而在jdk1.5中提供了一種新的同步方式--顯示鎖(Lock)。顯示鎖是隨java.util.concurrent包一起釋出的,java.util.concurrent包是併發大神Doug Lea寫的一個併發工具包,裡面除了顯示鎖,還有許多其他的實用併發工具類。
什麼是顯示鎖
什麼是顯示鎖?用一段程式碼來說明:
package com.gome; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockPractice { private static int a=0; private static Lock lock=new ReentrantLock(); public static void increateBySynchronized(){ a=0; for (int i = 0; i < 1000; i++) { Thread t=new Thread(new Runnable() { public void run() { for (int j = 0; j < 100; j++) { synchronized (LockPractice.class) { a++; } } } }); t.start(); } while (Thread.activeCount()>1) { Thread.yield(); } System.out.println(a); } public static void increateByLock(){ a=0; for (int i = 0; i < 1000; i++) { Thread t=new Thread(new Runnable() { public void run() { for (int j = 0; j < 100; j++) { lock.lock(); try { a++; } finally { lock.unlock(); } } } }); t.start(); } while (Thread.activeCount()>1) { Thread.yield(); } System.out.println(a); } public static void main(String[] args) { increateBySynchronized(); increateByLock(); } }
執行結果:
解釋:
類LockPractice 中有兩個方法increateBySynchronized()和increateByLock(),這兩個方法都成功地用多執行緒併發將a累加到100000而沒有出現競態條件(race condition)問題。
其中increateBySynchronized()的同步是我們熟悉的synchronized關鍵字實現的:
synchronized (LockPractice.class) { a++; }
這句程式碼的含義是:當有一個執行緒A進入synchronized程式碼塊後,阻塞其他要進入該程式碼塊的執行緒直到A執行完程式碼塊。synchronized關鍵字會關聯一個鎖物件,這裡是LockPractice.class。synchronized關鍵字底層是由jvm來實現的,當一個執行緒進入synchronized塊時,會在關聯的鎖物件的物件頭(MarkWord)中記錄下執行緒資訊(可以簡單的理解為執行緒id),這樣這個鎖物件就被當前執行緒獨佔了,其他試圖獲取這個鎖物件的執行緒將被阻塞。
因此一個執行緒進入、退出synchronized程式碼塊的本質就是這個執行緒對鎖物件的獲取、釋放。
而increateByLock()的同步程式碼如下,其中 lock是全域性變數 private static Lock lock=new ReentrantLock();
lock.lock(); try { a++; } finally { lock.unlock(); }
從程式碼上可以看出,顯示鎖Lock的使用和synchronized的本質很像,也是定義了一個鎖物件(new ReentrantLock()),然後在進入同步程式碼前加鎖,執行同步程式碼後釋放鎖。
但是顯示鎖的底層卻和synchronized完全不同,並沒有使用到物件頭(MarkWord)這樣底層的東西,顯示鎖只是表現出了和synchronized一樣的行為(第一個訪問同步程式碼的執行緒獲得鎖,阻塞後來的執行緒)。
顯示鎖的優點
從上面的描述來看,顯示鎖實現了和synchronized一樣的功能,但是寫起來更復雜(需要手動加鎖解鎖,還需要寫finally防止發生異常後鎖不能釋放),那為什麼還要加入顯示鎖呢?
我們可以從Lock介面提供的方法看出端倪:
方法名稱 | 描述 |
void lock() | 獲取鎖 |
void lockInterruptibly() throws InterruptedException | 可中斷地獲取鎖,線上程獲取鎖的過程中可以響應中斷 |
boolean tryLock() | 嘗試非阻塞獲取鎖,呼叫方法後立即返回,成功返回true,失敗返回false |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 在超時時間內獲取鎖,到達超時時間將返回false,也可以響應中斷 |
void unlock(); | 釋放鎖 |
Condition newCondition(); | 獲取等待元件,等待元件實現類似於Object.wait()方法的功能 |
從Lock提供的介面可以看出來,顯示鎖至少比synchronized多了以下功能:
- 可中斷獲取鎖:使用synchronized關鍵字獲取鎖的時候,如果執行緒沒有獲取到被阻塞了,那麼這個時候該執行緒是不響應中斷(interrupt)的,而使用Lock.lockInterruptibly()獲取鎖時被中斷,執行緒將丟擲中斷異常。
- 可非阻塞獲取鎖:使用sync關鍵字獲取鎖時,如果沒有成功獲取,只有被阻塞,而使用Lock.tryLock()獲取鎖時,如果沒有獲取成功也不會阻塞而是直接返回false。
- 可限定獲取鎖的超時時間:使用Lock.tryLock(long time, TimeUnit unit)。
- 其實顯示鎖還有其他的優勢,比如同一鎖物件上可以有多個等待佇列(相當於Object.wait()),我們後面會講。
其實除了更多的功能,顯示鎖還有一個很大的優勢:synchronized的同步是jvm底層實現的,對一般程式設計師來說程式遇到出乎意料的行為的時候,除了查官方文件幾乎沒有別的辦法;而顯示鎖除了個別操作用了底層的Unsafe類之外,幾乎都是用java語言實現的,我們可以通過學習顯示鎖的原始碼,來更加得心應手的使用顯示鎖。
顯示鎖的缺點
當然顯示鎖也不是完美的,否則java就不會保留著synchronized關鍵字了,顯示鎖的缺點主要有兩個:
- 使用比較複雜,這點之前提到了,需要手動加鎖,解鎖,而且還必須保證在異常狀態下也要能夠解鎖。而synchronized的使用就簡單多了。
- 效率較低,synchronized關鍵字畢竟是jvm底層實現的,因此用了很多優化措施來優化速度(偏向鎖、輕量鎖等),而顯示鎖的效率相對低一些。
因此當需要進行同步時,優先考慮使用synchronized關鍵字,只有synchronized關鍵字不能滿足需求時,才考慮使用顯示鎖。
總結
這篇文章介紹了顯示鎖是什麼,顯示鎖的優點與缺點,在什麼情況下會用到顯示鎖。
後文將重點學習顯示鎖的底層實現:佇列同步器(AbstractQueuedSynchronizer)的實現、重入鎖(ReentrantLock)的實現、讀寫鎖(ReadWriteLock)的實現、等待/通知(Condition)的實現。