大家好伶选,我是小黑史飞,一個(gè)在互聯(lián)網(wǎng)茍且偷生的農(nóng)民工。
在之前的文章中仰税,為了保證在并發(fā)情況下多線程共享數(shù)據(jù)的線程安全构资,我們會使用synchronized關(guān)鍵字來修飾方法或者代碼塊,以及在生產(chǎn)者消費(fèi)者模式中同樣使用synchronized來保證生產(chǎn)者和消費(fèi)者對于緩沖區(qū)的原子操作陨簇。
synchronized的缺點(diǎn)
那么synchronized這么厲害吐绵,到底有沒有什么缺點(diǎn)呢?主要有以下幾個(gè)方面:
- 使用synchronized加鎖的代碼塊或者方法河绽,在線程獲取鎖時(shí)拦赠,會一直試圖獲取直到獲取成功,不能中斷葵姥。
- 加鎖的條件只能在一個(gè)鎖對象上荷鼠,不支持其他條件
- 無法知道鎖對象的狀態(tài),是否被鎖
- synchronized鎖只支持非公平鎖榔幸,無法做到公平
- 對于讀操作和寫操作都是使用獨(dú)占鎖允乐,無法支持共享鎖(在讀操作時(shí)共享,寫操作時(shí)獨(dú)占)
- synchronized鎖在升級之后不支持降級削咆,如在業(yè)務(wù)流量高峰階段升級為重量級鎖牍疏,流量降低時(shí)還是重量級,效率較低(有些JVM實(shí)現(xiàn)支持降級拨齐,但是降級條件極為苛刻鳞陨,對于Java線程來說可基本認(rèn)為是不支持降級)
- 線程間通信無法按條件進(jìn)行線程的喚醒,如生產(chǎn)者消費(fèi)者場景中生產(chǎn)者完成數(shù)據(jù)生產(chǎn)后無法做到只喚醒消費(fèi)者瞻惋,其他等待的生產(chǎn)者也會被同時(shí)喚醒
以上是我能想到的synchronized鎖的一些缺點(diǎn)厦滤,如果你有不同的看法援岩,歡迎私信交流。(沒有留言板的痛/(ㄒoㄒ)/~~)
那么synchronized的這些問題該如何解決呢掏导?或者有沒有替代方案享怀?答案是有的,就是使用我們今天要講的Lock鎖趟咆。
Lock的優(yōu)點(diǎn)
Lock鎖是Java.util.concurrent.locks(JUC)包中的一個(gè)接口添瓷,并且有很多不同的實(shí)現(xiàn)類。這些實(shí)現(xiàn)類基本可以完全解決上面我們說到的所有問題值纱。
Lock鎖具備以下優(yōu)點(diǎn):
- 支持超時(shí)獲取鳞贷,中斷獲取
- 可以按條件加鎖,靈活性更高
- 支持公平和非公平鎖
- 有獨(dú)占鎖和共享鎖的實(shí)現(xiàn)虐唠,如讀寫鎖
- 可以做到等待線程的精準(zhǔn)喚醒
接下來具體看看對應(yīng)的實(shí)現(xiàn)搀愧。
基礎(chǔ)鋪墊
在開始之前,先和大家對于一些概念做一下回顧和普及凿滤。
可重入鎖
可重入鎖是指鎖具備可重入的特性妈橄,可重入的意思是一個(gè)線程在獲取鎖之后庶近,如果再次獲取鎖時(shí)翁脆,可以成功獲取,不會因?yàn)殒i正在被占有而死鎖鼻种。
synchronized鎖就是可重入鎖反番,在一個(gè)synchronized方法中遞歸調(diào)用本方法,可以成功獲取到鎖叉钥,不會死鎖罢缸。
Lock鎖的實(shí)現(xiàn)中基本也都支持可重入。
公平鎖和非公平鎖
公平鎖指在有線程獲取鎖失敗阻塞時(shí)投队,一定會讓先開始阻塞的線程先執(zhí)行枫疆,就好比是排隊(duì)買票,排在前面的先買敷鸦;
非公平鎖則不保證這種公平性息楔,就算有其他線程在阻塞等待,新來的線程也可以直接獲取鎖扒披,就好比插隊(duì)值依。
獨(dú)占鎖和共享鎖
獨(dú)占鎖是指一把鎖同一時(shí)間只能被一個(gè)線程持有,舉個(gè)生活中的例子碟案,我們使用打車軟件打?qū)\囋赶眨敲匆惠v車同一時(shí)間只能讓一個(gè)用戶打到,這輛專車就好比是一把獨(dú)占鎖价说,被一個(gè)用戶獨(dú)自占有了嘛辆亏。
共享鎖則不一樣风秤,一把鎖可以被多個(gè)線程持有,這個(gè)就想我們打拼車褒链,一輛拼車同一時(shí)間可以讓多個(gè)用戶打到唁情,這輛拼車就是一把共享鎖。
說完這些以后我們來看一下Lock接口的一些具體實(shí)現(xiàn)甫匹。
ReentrantLock
ReentrantLock從名稱理解甸鸟,就是一把可重入鎖,并且它是一把獨(dú)占鎖兵迅,而且具有公平和非公平實(shí)現(xiàn)抢韭。
我們通過代碼來看一下如何通過ReentrantLock來做加解鎖操作。
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock(false);
lock.lock();
try {
// do something...
}finally {
lock.unlock();
}
}
首先創(chuàng)建一個(gè)ReentrantLock對象恍箭,在創(chuàng)建時(shí)構(gòu)造方法傳入的boolean值控制是公平鎖還是非公平鎖刻恭,如果不傳參數(shù)則默認(rèn)是非公平鎖。
調(diào)用lock()方法來進(jìn)行加鎖扯夭,可以看到使用try-finally代碼塊鳍贾,在finally中進(jìn)行unlock()解鎖操作,這一點(diǎn)一定要注意交洗,因?yàn)閘ock不會自己進(jìn)行解鎖骑科,必須手動進(jìn)行釋放,為了保證鎖一定可以被釋放构拳,防止發(fā)生死鎖咆爽,所以要在finally中進(jìn)行。這一點(diǎn)和synchronized有區(qū)別置森,使用synchronized不用關(guān)注鎖的釋放時(shí)機(jī)斗埂,這也是為了靈活性必須要付出的一點(diǎn)代價(jià)。
ReentrantLock除了通過lock()方法加鎖之外凫海,還有以下方式加鎖:
- tryLock():只有在調(diào)用時(shí)它不被另一個(gè)線程占用才能獲取鎖
- tryLock(long timeout, TimeUnit unit) 如果在給定的等待時(shí)間內(nèi)沒有被另一個(gè)線程占用呛凶,并且當(dāng)前線程尚未被中斷,則獲取該鎖
- lockInterruptibly() 獲取鎖定行贪,除非當(dāng)前線程是interrupted
除了獲取鎖的方法之外漾稀,還有一些其他的方法可以獲得一些鎖相關(guān)的狀態(tài)信息:
- isLocked() 查詢此鎖是否由任何線程持有
- isHeldByCurrentThread() 查詢此鎖是否由當(dāng)前線程持有
- getOwner() 返回當(dāng)前擁有此鎖的線程,如果不擁有瓮顽,則返回null
ReentrantLock本身是獨(dú)占鎖县好,不支持共享,那么如何做到線程的精準(zhǔn)喚醒暖混,我們接著說缕贡。
Condition
Condition也是JUC包下的locks包中的一個(gè)接口,提供了類似于Object的wait(),notify()晾咪,notifyAll()這樣的對象監(jiān)聽器方法收擦,可以與Lock的實(shí)現(xiàn)類配合做到線程的等待/喚醒機(jī)制,并且能夠做到精準(zhǔn)喚醒谍倦。接下來我們看下面的例子:
public class ProdConsDemo {
public static void main(String[] args) {
KFC kfc = new KFC();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.product();
}
}, "店員1").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.product();
}
}, "店員2").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.consume();
}
}, "顧客1").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
kfc.consume();
}
}, "顧客2").start();
}
}
class KFC {
int hamburgerNum = 0;
public synchronized void product() {
while (hamburgerNum == 10) {
try {
// 數(shù)量到達(dá)最大塞赂,生產(chǎn)者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生產(chǎn)一個(gè)漢堡" + (++hamburgerNum));
// 喚醒其他線程
this.notifyAll();
}
public synchronized void consume() {
while (hamburgerNum == 0) {
try {
//數(shù)量到達(dá)最小,消費(fèi)者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("賣出一個(gè)漢堡" + (hamburgerNum--));
// 喚醒其他線程
this.notifyAll();
}
}
看過小黑之前文章的朋友應(yīng)該還記得這個(gè)例子昼蛀,KFC里的店員生產(chǎn)漢堡宴猾,顧客來消費(fèi),典型的生產(chǎn)者消費(fèi)者模式叼旋,我們可以看到在上面的代碼中仇哆,是使用的鎖對象this的wait()和notifyAll()方法來做線程等待和喚醒。那么這里會有一個(gè)問題夫植,就是在notifyAll()時(shí)讹剔,無法做到只喚醒消費(fèi)者或者只喚醒生產(chǎn)者。而在線程被喚醒之后就會面臨更多的線程切換详民,而線程切換是很消耗CPU資源的延欠。
那么我們使用Condition和ReentrantLock來修改一下我們的代碼。
class KFC {
int hamburgerNum = 0;
ReentrantLock lock = new ReentrantLock();
Condition isEmpty = lock.newCondition();
Condition isFull = lock.newCondition();
public void product() {
lock.lock();
try {
while (hamburgerNum == 10) {
// 數(shù)量到達(dá)最大沈跨,生產(chǎn)者等待
isFull.await();
}
System.out.println("生產(chǎn)一個(gè)漢堡" + (++hamburgerNum));
// 喚醒消費(fèi)者線程
isEmpty.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
while (hamburgerNum == 0) {
//數(shù)量到達(dá)最小由捎,消費(fèi)者等待
isEmpty.await();
}
System.out.println("賣出一個(gè)漢堡" + (hamburgerNum--));
// 喚醒生產(chǎn)者線程
isFull.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
可以看到,我們使用ReentrantLock來進(jìn)行線程安全控制谒出,進(jìn)行加解鎖隅俘,然后創(chuàng)建兩個(gè)Condition對象邻奠,分別代表生產(chǎn)者和消費(fèi)者的標(biāo)記笤喳,當(dāng)生產(chǎn)者生產(chǎn)完一個(gè)之后,就會準(zhǔn)確的喚醒消費(fèi)者線程碌宴,反之同理杀狡。
ReadWriteLock
ReadWriteLock是讀寫鎖接口,通過ReadWriteLock可以實(shí)現(xiàn)多個(gè)線程對于讀操作共享贰镣,對于寫操作獨(dú)占呜象。
在ReadWriteLock中有兩個(gè)Lock變量,通過兩個(gè)Lock分別控制讀和寫碑隆。
class Data {
private int num = 0;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
public void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "讀取=>" + num);
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "讀取結(jié)束");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "寫入=>" + num++);
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "寫入結(jié)束");
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}
對于讀寫鎖恭陡,線程對于鎖的競爭情況如下:
- 讀-讀操作共享;
- 讀-寫操作獨(dú)占上煤;
- 寫-讀操作獨(dú)占休玩;
- 寫-寫操作獨(dú)占;
也就是說,當(dāng)有一個(gè)線程持有讀鎖時(shí)拴疤,其他線程也可以獲取讀到讀鎖永部,但是不能獲取寫鎖,必須等讀鎖釋放呐矾;當(dāng)有一個(gè)線程持有寫鎖時(shí)苔埋,其他線程都不能獲取到鎖。
StampedLock
StampedLock是JDK1.8新引入的蜒犯,主要是為了優(yōu)化ReadWriteLock的讀寫鎖性能组橄,相比于普通的ReadWriteLock主要多了樂觀獲取讀鎖的功能。
那么ReadWriteLock有什么性能問題呢罚随?主要出現(xiàn)在讀-寫操作上晨炕,當(dāng)有一個(gè)線程在讀取時(shí),寫線程只能等讀取完之后才能獲取毫炉,讀的過程中不允許寫瓮栗,是一個(gè)悲觀讀鎖。
StampedLock允許在讀的過程中寫瞄勾,但是這樣會導(dǎo)致我們讀線程獲取的數(shù)據(jù)不一致费奸,所以需要增加一點(diǎn)代碼來判斷在讀的過程中是否有些操作,這是一種樂觀讀的鎖进陡;我們來看一下代碼愿阐。
class Data {
private int num = 0;
private final StampedLock lock = new StampedLock();
public void read() {
// long stamp = lock.readLock();
// 獲取樂觀讀,拿到一個(gè)版本戳
long stamp = lock.tryOptimisticRead();
try {
System.out.println(Thread.currentThread().getName() + "讀取=>" + num);
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "讀取結(jié)束");
// 讀取完之后對剛開始拿到的版本戳進(jìn)行驗(yàn)證
if (!lock.validate(stamp)) {
// 驗(yàn)證不通過趾疚,說明發(fā)生了寫操作缨历,這是需要重新獲取悲觀讀鎖進(jìn)行處理
System.out.println("validatefalse");
stamp = lock.readLock();
// do something...
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlockRead(stamp);
}
}
public void write() {
long stamp = lock.writeLock();
try {
System.out.println(Thread.currentThread().getName() + "寫入=>" + num++);
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "寫入結(jié)束");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlockWrite(stamp);
}
}
}
所以StampedLock就是先樂觀的認(rèn)為在讀的過程中不會有寫操作,所以是樂觀鎖糙麦,而悲觀鎖就是悲觀的認(rèn)為在讀的過程中會有些操作辛孵,所以拒絕寫入。
顯然在并發(fā)高的情況下樂觀鎖的并發(fā)效率要更高赡磅,但是會有一小部分的寫入導(dǎo)致數(shù)據(jù)不準(zhǔn)確魄缚,所以需要通過validate(stamp)檢測出來,重新讀取焚廊。
總結(jié)
簡單總結(jié)一下冶匹,首先我們講了synchronized的7個(gè)缺點(diǎn):不能超時(shí)中斷;只能在一個(gè)對象上加鎖咆瘟;獲取不到鎖的狀態(tài)嚼隘;不支持公平鎖;不支持共享鎖;鎖升級后不能降級;無法做到精準(zhǔn)喚醒阻塞線程等。
然后我們通過Lock的具體實(shí)現(xiàn)看到合敦,Lock都解決了這些問題桩皿,ReentrantLock支持超時(shí)中斷獲取鎖豌汇,并且可以按條件判斷進(jìn)行加鎖,有方法可以看到鎖的狀態(tài)信息泄隔,支持公平和非公平實(shí)現(xiàn)等拒贱,通過Condition的await()和signal()/signalAll()可以做到精準(zhǔn)喚醒等待線程;ReadWriteLock可以支持共享鎖佛嬉,讀鎖共享逻澳,寫鎖獨(dú)占;然后StampedLock在性能上對讀寫鎖進(jìn)行優(yōu)化暖呕,主要是通過樂觀讀鎖和vaidate(stamp)驗(yàn)證讀取過程中有沒有寫入斜做。
使用Lock鎖很重要的一點(diǎn)就是需要自己手動釋放鎖,所以一定要寫在finally中湾揽;
使用Conditon進(jìn)行喚醒線程時(shí)要記清楚是signal()/signalAll()方法瓤逼,不是notify()/notifyAll()方法,不要用錯(cuò)了库物。
Lock鎖的底層實(shí)現(xiàn)邏輯都是依賴于AbstractQueuedSynchronizer(AQS)和CAS無鎖機(jī)制來實(shí)現(xiàn)的霸旗,這部分內(nèi)容比較復(fù)雜,我們下期單獨(dú)來說一說戚揭。
好的诱告,今天的內(nèi)容就到這里,我們下期見民晒。