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,前者的先進性體現在以下幾點:
可響應中斷的鎖。當在等待鎖的線程如果長期得不到鎖,那麼可以選擇不繼續等待而去處理其他事情,而synchronized的互斥鎖則必須阻塞等待,不能被中斷 可實現公平鎖。所謂公平鎖指的是多個線程在等待鎖的時候必須按照線程申請鎖的時間排隊等待,而非公平性鎖則保證這點,每個線程都有獲得鎖的機會。synchronized的鎖和ReentrantLock使用的默認鎖都是非公平性鎖,但是ReentrantLock支持公平性的鎖,在構造函數中傳入一個boolean變量指定為true實現的就是公平性鎖。不過一般而言,使用非公平性鎖的性能優於使用公平性鎖 每個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秒後,主線程中斷讀線程的時候讀線程應該能夠正常響應中斷,然後停止執行讀數據的操作。我們看看代碼:<喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;">
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();
}
}
運行結果如下:
顯然,讀線程正常響應了我們的中斷,因為讀線程輸出了中斷信息,即使寫線程寫完數據後,讀線程也沒有輸出結束讀數據的信息,這點是在我們意料之中的。這樣也驗證了可中斷鎖的分析。