Java 5 之后桨昙,Java在內(nèi)置關(guān)鍵字sychronized的基礎(chǔ)上又增加了一個(gè)新的處理鎖的方式,Lock類(lèi)工秩。
由于在Java線程間通信:volatile與sychronized中廉白,我們已經(jīng)詳細(xì)的了解了synchronized,所以我們現(xiàn)在主要介紹一下Lock乖菱,以及將Lock與synchronized進(jìn)行一下對(duì)比坡锡。
- 1. synchronized的缺陷
- 2. Lock類(lèi)接口設(shè)計(jì)
- 3. ReentrantLock可重入鎖
- 4. ReadWriteLock讀寫(xiě)鎖
- 5. 公平鎖
- 6. Lock和synchronized的選擇
- 7. 參考文章
1. synchronized的缺陷
synchronized修飾的代碼只有獲取鎖的線程才能夠執(zhí)行蓬网,其他線程只能等待該線程釋放鎖。一個(gè)線程釋放鎖的情況有以下方式:
- 獲取鎖的線程完成了synchronized修飾的代碼塊的執(zhí)行鹉勒。
- 線程執(zhí)行時(shí)發(fā)生異常帆锋,JVM自動(dòng)釋放鎖。
我們?cè)?a target="_blank" rel="nofollow">Java多線程的生命周期禽额,實(shí)現(xiàn)與調(diào)度中談過(guò)锯厢,鎖會(huì)因?yàn)榈却齀/O,sleep()方法等原因被阻塞而不釋放鎖脯倒,此時(shí)如果線程還處于用synchronized修飾的代碼區(qū)域里实辑,那么其他線程只能等待,這樣就影響了效率藻丢。因此Java提供了Lock來(lái)實(shí)現(xiàn)另一個(gè)機(jī)制剪撬,即不讓線程無(wú)限期的等待下去。
思考一個(gè)情景悠反,當(dāng)多線程讀寫(xiě)文件時(shí)残黑,讀操作和寫(xiě)操作會(huì)發(fā)生沖突,寫(xiě)操作和寫(xiě)操作會(huì)發(fā)生沖突斋否,但讀操作和讀操作不會(huì)有沖突梨水。如果使用synchronized來(lái)修飾的話,就很可能造成多個(gè)讀操作無(wú)法同時(shí)進(jìn)行的可能(如果只用synchronized修飾寫(xiě)方法如叼,那么可能造成讀寫(xiě)沖突冰木,如果同時(shí)修飾了讀寫(xiě)方法,則會(huì)有讀讀干擾)笼恰。此時(shí)就需要用到Lock踊沸,換言之Lock比synchronized提供了更多的功能。
使用Lock需要注意以下兩點(diǎn):
- Lock不是語(yǔ)言內(nèi)置的社证,synchronized是Java關(guān)鍵字逼龟,為內(nèi)置特性,Lock是一個(gè)類(lèi)追葡,通過(guò)這個(gè)類(lèi)可以實(shí)現(xiàn)同步訪問(wèn)腺律。
- 采用synchronized時(shí)我們不需要手動(dòng)去控制加鎖和釋放,系統(tǒng)會(huì)自動(dòng)控制宜肉。而使用Lock類(lèi)匀钧,我們需要手動(dòng)的加鎖和釋放,不主動(dòng)釋放可能會(huì)造成死鎖谬返。實(shí)際上Lock類(lèi)的使用某種意義上講要比synchronized更加直觀之斯。
2. Lock類(lèi)接口設(shè)計(jì)
Lock類(lèi)本身是一個(gè)接口,其方法如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
下面依次講解一下其中各個(gè)方法遣铝。
-
lock() 方法使用最多佑刷,作用是用于獲取鎖莉擒,如果鎖已經(jīng)被其他線程獲得,則等待瘫絮。
通常情況下涨冀,lock使用以下方式去獲取鎖:
Lock lock = ...;
lock.lock();
try{
//處理任務(wù)
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
-
lockInterruptibly() 和lock()的區(qū)別是lockInterruptibly()鎖定的線程處于等待狀態(tài)時(shí),允許線程的打斷操作麦萤,線程使用Thread.interrupt()打斷該線程后會(huì)直接返回并拋出一個(gè)InterruptException()鹿鳖;lock()方法鎖定對(duì)象時(shí)如果在等待時(shí)檢測(cè)到線程使用Thread.interrupt(),仍然會(huì)繼續(xù)嘗試獲取鎖频鉴,失敗則繼續(xù)休眠棒坏,只是在成功獲取鎖之后在把當(dāng)前線程置為interrupt狀態(tài)验游。也就使說(shuō)陆馁,當(dāng)兩個(gè)線程同時(shí)通過(guò)lockInterruptibly()想獲取某個(gè)鎖時(shí)雷逆,假若此時(shí)線程A獲取到了鎖吧黄,而線程B只有在等待驹溃,那么對(duì)線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過(guò)程舱殿。
因此饲宛,lockInterruptibly()方法必須實(shí)現(xiàn)catch(InterruptException e)代碼塊僵娃。常見(jiàn)使用方式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
-
tryLock() 和lock()最大的不同是具有返回值概作,或者說(shuō),它不去等待鎖默怨。如果它成功獲取鎖讯榕,那么返回true;如果它無(wú)法成功獲取鎖匙睹,則返回false愚屁。
通常情況下,tryLock使用方式如下:
Lock lock = ...;
if(lock.tryLock()) {
try{
//處理任務(wù)
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
}else {
//如果不能獲取鎖痕檬,則直接做其他事情
}
- tryLock(long time, TimeUnit unit) 則是介于二者之間霎槐,用戶設(shè)定一個(gè)等待時(shí)間,如果在這個(gè)時(shí)間內(nèi)獲取到了鎖梦谜,則返回true丘跌,否則返回false結(jié)束。
- unlock() 從上面的代碼里我們也看到唁桩,unlock()一般放在異常處理操作的finally字符控制的代碼塊中闭树。我們要記得Lock和sychronized的區(qū)別,防止產(chǎn)生死鎖荒澡。
- newCondition() 該方法我們放到后面講报辱。
3. ReentrantLock可重入鎖
3.1. ReentrantLock概述
ReentrantLock譯為“可重入鎖”,我們?cè)?a target="_blank" rel="nofollow">Java多線程:synchronized的可重入性中已經(jīng)明白了什么是可重入以及理解了synchronized的可重入性仰猖。ReentrantLock是唯一實(shí)現(xiàn)Lock接口的類(lèi)捏肢。
3.2. ReentrantLock使用
考慮到以下情景奈籽,一個(gè)僅出售雙人票的演唱會(huì)進(jìn)行門(mén)票出售,有三個(gè)售票口同時(shí)進(jìn)行售票鸵赫,買(mǎi)票需要100ms時(shí)間衣屏,每張票出票需要100ms時(shí)間。該如何設(shè)計(jì)這個(gè)情景辩棒?
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;//保證從主內(nèi)存獲取
SoldTicket(Integer ticket) {
this.ticket = ticket;//提供票數(shù)
}
private void sold() {
lock.lock();//鎖定操作放在try代碼塊外
try {
if (ticket <= 0) return;//當(dāng)ticket==2時(shí)可能有多個(gè)線程進(jìn)入sold方法狼忱,一個(gè)線程運(yùn)行后另外兩個(gè)線程需要退出。
sleep(200);//買(mǎi)票0.1s,出票0.1s
--ticket;
System.out.println("The first ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave.");//獲取線程id來(lái)識(shí)別出票站一睁。
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();
}
}
上面這段代碼結(jié)果如下:
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.
如果我們不對(duì)售票操作進(jìn)行鎖定钻弄,則會(huì)有以下幾個(gè)問(wèn)題:
- 出售第一張票后其他機(jī)器出了另一張票,導(dǎo)致票沒(méi)有成對(duì)賣(mài)者吁。
- 已經(jīng)無(wú)票后仍有機(jī)器出票造成混亂窘俺。
顯然,本題的情景用synchronized也可以很容易的實(shí)現(xiàn)复凳,實(shí)際上Lock有別于synchronized的主要點(diǎn)是lockInterruptibly()和tryLock()這兩個(gè)可以對(duì)鎖進(jìn)行控制的方法瘤泪。
4. ReadWriteLock讀寫(xiě)鎖
4.1. ReadWriteLock接口
回到開(kāi)頭synchronized缺陷的介紹,實(shí)際上育八,Lock接口的重要衍生接口ReadWriteLock即是解決這一問(wèn)題对途。ReadWriteLock定義很簡(jiǎn)單,僅有兩個(gè)接口:
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()兩個(gè)操作髓棋,這兩個(gè)操作均返回一個(gè)Lock類(lèi)的實(shí)例实檀。兩個(gè)操作一個(gè)獲取讀鎖,一個(gè)獲取寫(xiě)鎖按声,將讀寫(xiě)分開(kāi)進(jìn)行操作膳犹。ReadWriteLock將讀寫(xiě)的鎖分開(kāi),可以讓多個(gè)讀操作并行儒喊,這就大大提高了效率镣奋。使用ReadWriteLock時(shí),用讀鎖去控制讀操作怀愧,寫(xiě)鎖控制寫(xiě)操作侨颈,進(jìn)而實(shí)現(xiàn)了一個(gè)可以在如下的大量讀少量寫(xiě)且讀者優(yōu)先的情景運(yùn)行的鎖。
4.2. ReentrantReadWriteLock可重入讀寫(xiě)鎖
ReentrantReadWriteLock是ReadWriteLock的唯一實(shí)例芯义。同時(shí)提供了很多操作方法哈垢。ReentratReadWriteLock接口實(shí)現(xiàn)的讀鎖寫(xiě)鎖進(jìn)入有如下要求:
4.2.1. 線程進(jìn)入讀鎖的要求
- 沒(méi)有其他線程的寫(xiě)鎖。
- 沒(méi)有鎖請(qǐng)求 或 調(diào)用寫(xiě)請(qǐng)求的線程正是該線程扛拨。
4.2.2. 線程進(jìn)入寫(xiě)鎖的要求
- 沒(méi)有其他線程的讀鎖耘分。
- 沒(méi)有其他線程的寫(xiě)鎖。
4.2.3. 讀寫(xiě)鎖使用示例
private SomeClass someClass;//資源
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//創(chuàng)建鎖
private final Lock readLock = readWriteLock.readLock();//讀鎖
private final Lock writeLock = readWriteLock.writeLock();//寫(xiě)鎖
//讀方法
readLock.lock();
try {
result = someClass.someMethod();
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
//寫(xiě)方法,產(chǎn)生新的SomeClass實(shí)例tempSomeClass
writeLock.lock();
try{
this.someClass = tempSomeClass;//更新
}catch (Exception e) {
e.printStackTrace();
} finally{
writeLock.unlock();
}
5. 公平鎖
公平鎖即當(dāng)多個(gè)線程等待的一個(gè)資源的鎖釋放時(shí)求泰,線程不是隨機(jī)的獲取資源而是等待時(shí)間最久的線程獲取資源(FIFO)央渣。Java中,synchronized是一個(gè)非公平鎖渴频,無(wú)法保證鎖的獲取順序芽丹。ReentrantLock和ReentrantReadWriteLock默認(rèn)也是非公平鎖,但可以設(shè)置成公平鎖卜朗。我們前面的實(shí)例中初始化ReentrantLock和ReentrantReadWriteLock時(shí)都是無(wú)參數(shù)的拔第。實(shí)際上,它們提供一個(gè)默認(rèn)的boolean變量fair场钉,為true則為公平鎖蚊俺,為false則為非公平鎖,默認(rèn)為false逛万。因此泳猬,當(dāng)我們想將其實(shí)現(xiàn)為公平鎖時(shí),僅需要初始化時(shí)賦值true泣港。即:
Lock lock = new ReentrantLock(true);
考慮前面賣(mài)票的實(shí)例暂殖,如果改為公平鎖(盡管這和情景無(wú)關(guān))价匠,則結(jié)果輸出非常整齊如下:
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是內(nèi)置語(yǔ)言實(shí)現(xiàn)的關(guān)鍵字当纱,Lock是為了實(shí)現(xiàn)更高級(jí)鎖功能而提供的接口。
- synchronized發(fā)生異常時(shí)自動(dòng)釋放占有的鎖踩窖,Lock需要在finally塊中手動(dòng)釋放鎖坡氯。因此從安全性角度講,既可以用Lock又可以用synchronized時(shí)(即不需要鎖的更高級(jí)功能時(shí))使用synchronized更保險(xiǎn)洋腮。
- 線程激烈競(jìng)爭(zhēng)時(shí)Lock的性能遠(yuǎn)優(yōu)于synchronized箫柳,即有大量線程時(shí)推薦使用Lock。
- Lock可以通過(guò)lockInterruptibly()接口實(shí)現(xiàn)可中斷鎖啥供。
- ReentrantReadWriteLock實(shí)現(xiàn)了封裝好的讀寫(xiě)鎖用于大量讀少量寫(xiě)讀者優(yōu)先情景解決了synchronized讀寫(xiě)情景難以實(shí)現(xiàn)問(wèn)題悯恍。