Java 鎖的知識(shí)總結(jié)及實(shí)例代碼
java中有哪些鎖
這個(gè)問(wèn)題在我看了一遍<java并發(fā)編程>后盡然無(wú)法回答,說(shuō)明自己對(duì)于鎖的概念了解的不夠。于是再次翻看了一下書(shū)里的內(nèi)容,突然有點(diǎn)打開(kāi)腦門(mén)的感覺(jué)??磥?lái)確實(shí)是要學(xué)習(xí)的最好方式是要帶著問(wèn)題去學(xué),并且解決問(wèn)題。
在java中鎖主要兩類(lèi):內(nèi)部鎖synchronized和顯示鎖java.util.concurrent.locks.Lock。但細(xì)細(xì)想這貌似總結(jié)的也不太對(duì)。應(yīng)該是由java內(nèi)置的鎖和concurrent實(shí)現(xiàn)的一系列鎖。
為什么這說(shuō),因?yàn)樵趈ava中一切都是對(duì)象,而java對(duì)每個(gè)對(duì)象都內(nèi)置了一個(gè)鎖,也可以稱(chēng)為對(duì)象鎖/內(nèi)部鎖。通過(guò)synchronized來(lái)完成相關(guān)的鎖操作。
而因?yàn)閟ynchronized的實(shí)現(xiàn)有些缺陷以及并發(fā)場(chǎng)景的復(fù)雜性,有人開(kāi)發(fā)了一種顯式的鎖,而這些鎖都是由java.util.concurrent.locks.Lock派生出來(lái)的。當(dāng)然目前已經(jīng)內(nèi)置到了JDK1.5及之后的版本中。
synchronized
首先來(lái)看看用的比較多的synchronized,我的日常工作中大多用的也是它。synchronized是用于為某個(gè)代碼塊的提供鎖機(jī)制,在java的對(duì)象中會(huì)隱式的擁有一個(gè)鎖,這個(gè)鎖被稱(chēng)為內(nèi)置鎖(intrinsic)或監(jiān)視器鎖(monitor locks)。線程在進(jìn)入被synchronized保護(hù)的塊之前自動(dòng)獲得這個(gè)鎖,直到完成代碼后(也可能是異常)自動(dòng)釋放鎖。內(nèi)置鎖是互斥的,一個(gè)鎖同時(shí)只能被一個(gè)線程持有,這也就會(huì)導(dǎo)致多線程下,鎖被持有后后面的線程會(huì)阻塞。正因此實(shí)現(xiàn)了對(duì)代碼的線程安全保證了原子性。
可重入
既然java內(nèi)置鎖是互斥的而且后面的線程會(huì)導(dǎo)致阻塞,那么如果持有鎖的線程再次進(jìn)入試圖獲得這個(gè)鎖時(shí)會(huì)如何呢?比如下面的一種情況:
public class BaseClass { public synchronized void do() { System.out.println("is base"); } } public class SonClass extends BaseClass { public synchronized void do() { System.out.println("is son"); super.do(); } } SonClass son = new SonClass(); son.do();
此時(shí)派生類(lèi)的do方法除了會(huì)首先會(huì)持有一次鎖,然后在調(diào)用super.do()的時(shí)候又會(huì)再一次進(jìn)入鎖并去持有,如果鎖是互斥的話(huà)此時(shí)就應(yīng)該死鎖了。
但結(jié)果卻不是這樣的,這是因?yàn)閮?nèi)部鎖是具有可重入的特性,也就是鎖實(shí)現(xiàn)了一個(gè)重入機(jī)制,引用計(jì)數(shù)管理。當(dāng)線程1持有了對(duì)象的鎖a,此時(shí)會(huì)對(duì)鎖a的引用計(jì)算加1。然后當(dāng)線程1再次獲得鎖a時(shí),線程1還是持有鎖a的那么計(jì)算會(huì)加1。當(dāng)然每次退出同步塊時(shí)會(huì)減1,直到為0時(shí)釋放鎖。
synchronized的一些特點(diǎn)
修飾代碼的方式
修飾方法
public class BaseClass { public synchronized void do() { System.out.println("is base"); } }
這種就是直接對(duì)某個(gè)方法進(jìn)行加鎖,進(jìn)入這個(gè)方法塊時(shí)需要獲得鎖。
修飾代碼塊
public class BaseClass { private static Object lock = new Object(); public void do() { synchronized (lock) { System.out.println("is base"); } } }
這里就將鎖的范圍減少到了方法中的部分代碼塊,這對(duì)于鎖的靈活性就提高了,畢竟鎖的粒度控制也是鎖的一個(gè)關(guān)鍵問(wèn)題。
對(duì)象鎖的類(lèi)型
經(jīng)??吹揭恍┐a中對(duì)synchronized使用比較特別,看一下如下的代碼:
public class BaseClass { private static Object lock = new Object(); public void do() { synchronized (lock) { } } public synchronized void doVoid() { } public synchronized static void doStaticVoid() { } public static void doStaticVoid() { synchronized (BaseClass.class) { } } }
這里出現(xiàn)了四種情況:修飾代碼塊,修飾了方法,修飾了靜態(tài)方法,修飾BaseClass的class對(duì)象。那這幾種情況會(huì)有什么不同呢?
修飾代碼塊
這種情況下我們創(chuàng)建了一個(gè)對(duì)象lock,在代碼中使用synchronized(lock)這種形式,它的意思是使用lock這個(gè)對(duì)象的內(nèi)置鎖。這種情況下就將鎖的控制交給了一個(gè)對(duì)象。當(dāng)然這種情況還有一種方式:
public void do() { synchronized (this) { System.out.println("is base"); } }
使用this的意思就是當(dāng)前對(duì)象的鎖。這里也道出了內(nèi)置鎖的關(guān)鍵,我提供一把鎖來(lái)保護(hù)這塊代碼,無(wú)論哪個(gè)線程來(lái)都面對(duì)同一把鎖咯。
修飾對(duì)象方法
這種直接修飾在方法是咱個(gè)情況?其實(shí)和修飾代碼塊類(lèi)似,只不過(guò)此時(shí)默認(rèn)使用的是this,也就是當(dāng)前對(duì)象的鎖。這樣寫(xiě)起代碼來(lái)倒也比較簡(jiǎn)單明確。前面說(shuō)過(guò)了與修飾代碼塊的區(qū)別主要還是控制粒度的區(qū)別。
修飾靜態(tài)方法
靜態(tài)方法難道有啥不一樣嗎?確實(shí)是不一樣的,此時(shí)獲取的鎖已經(jīng)不是this了,而this對(duì)象指向的class,也就是類(lèi)鎖。因?yàn)镴ava中的類(lèi)信息會(huì)加載到方法常量區(qū),全局是唯一的。這其實(shí)就提供了一種全局的鎖。
修飾類(lèi)的Class對(duì)象
這種情況其實(shí)和修改靜態(tài)方法時(shí)比較類(lèi)似,只不過(guò)還是一個(gè)道理這種方式可以提供更靈活的控制粒度。
小結(jié)
通過(guò)這幾種情況的分析與理解,其實(shí)可以看內(nèi)置鎖的主要核心理念就是為一塊代碼提供一個(gè)可以用于互斥的鎖,起到類(lèi)似于開(kāi)關(guān)的功能。
java中對(duì)內(nèi)置鎖也提供了一些實(shí)現(xiàn),主要的特點(diǎn)就是java都是對(duì)象,而每個(gè)對(duì)象都有鎖,所以可以根據(jù)情況選擇用什么樣的鎖。
java.util.concurrent.locks.Lock
前面看了synchronized,大部分的情況下差不多就夠啦,但是現(xiàn)在系統(tǒng)在并發(fā)編程中復(fù)雜性是越來(lái)越高,所以總是有許多場(chǎng)景synchronized處理起來(lái)會(huì)比較費(fèi)勁?;蛘呦?lt;java并發(fā)編程>中說(shuō)的那樣,concurrent中的lock是對(duì)內(nèi)部鎖的一種補(bǔ)充,提供了更多的一些高級(jí)特性。
java.util.concurrent.locks.Lock簡(jiǎn)單分析
這個(gè)接口抽象了鎖的主要操作,也因此讓從Lock派生的鎖具備了這些基本的特性:無(wú)條件的、可輪循的、定時(shí)的、可中斷的。而且加鎖與解鎖的操作都是顯式進(jìn)行。下面是它的代碼:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
ReentrantLock
ReentrantLock就是可重入鎖,連名字都這么顯式。ReentrantLock提供了和synchronized類(lèi)似的語(yǔ)義,但是ReentrantLock必須顯式的調(diào)用,比如:
public class BaseClass { private Lock lock = new ReentrantLock(); public void do() { lock.lock(); try { //.... } finally { lock.unlock(); } } }
這種方式對(duì)于代碼閱讀來(lái)說(shuō)還是比較清楚的,只不過(guò)有個(gè)問(wèn)題,就是如果忘了加try finally或忘 了寫(xiě)lock.unlock()的話(huà)導(dǎo)致鎖沒(méi)釋放,很有可能導(dǎo)致一些死鎖的情況,synchronized就沒(méi)有這個(gè)風(fēng)險(xiǎn)。
trylock
ReentrantLock是實(shí)現(xiàn)Lock接口,所以自然就擁有它的那些特性,其中就有trylock。trylock就是嘗試獲取鎖,如果鎖已經(jīng)被其他線程占用那么立即返回false,如果沒(méi)有那么應(yīng)該占用它并返回true,表示拿到鎖啦。
另一個(gè)trylock方法里帶了參數(shù),這個(gè)方法的作用是指定一個(gè)時(shí)間,表示在這個(gè)時(shí)間內(nèi)一直嘗試去獲得鎖,如果到時(shí)間還沒(méi)有拿到就放棄。
因?yàn)閠rylock對(duì)鎖并不是一直阻塞等待的,所以可以更多的規(guī)避死鎖的發(fā)生。
lockInterruptibly
lockInterruptibly是在線程獲取鎖時(shí)優(yōu)先響應(yīng)中斷,如果檢測(cè)到中斷拋出中斷異常由上層代碼去處理。這種情況下就為一種輪循的鎖提供了退出機(jī)制。為了更好理解可中斷的鎖操作,寫(xiě)了一個(gè)demo來(lái)理解。
package com.test; import java.util.Date; import java.util.concurrent.locks.ReentrantLock; public class TestLockInterruptibly { static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { doPrint("thread 1 get lock."); do123(); doPrint("thread 1 end."); } catch (InterruptedException e) { doPrint("thread 1 is interrupted."); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { doPrint("thread 2 get lock."); do123(); doPrint("thread 2 end."); } catch (InterruptedException e) { doPrint("thread 2 is interrupted."); } } }); thread1.setName("thread1"); thread2.setName("thread2"); thread1.start(); try { Thread.sleep(100);//等待一會(huì)使得thread1會(huì)在thread2前面執(zhí)行 } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); } private static void do123() throws InterruptedException { lock.lockInterruptibly(); doPrint(Thread.currentThread().getName() + " is locked."); try { doPrint(Thread.currentThread().getName() + " doSoming1...."); Thread.sleep(5000);//等待幾秒方便查看線程的先后順序 doPrint(Thread.currentThread().getName() + " doSoming2...."); doPrint(Thread.currentThread().getName() + " is finished."); } finally { lock.unlock(); } } private static void doPrint(String text) { System.out.println((new Date()).toLocaleString() + " : " + text); } }
上面代碼中有兩個(gè)線程,thread1比thread2更早啟動(dòng),為了能看到拿鎖的過(guò)程將上鎖的代碼sleep了5秒鐘,這樣就可以感受到前后兩個(gè)線程進(jìn)入獲取鎖的過(guò)程。最終上面的代碼運(yùn)行結(jié)果如下:
2016-9-28 15:12:56 : thread 1 get lock.
2016-9-28 15:12:56 : thread1 is locked.
2016-9-28 15:12:56 : thread1 doSoming1....
2016-9-28 15:12:56 : thread 2 get lock.
2016-9-28 15:13:01 : thread1 doSoming2....
2016-9-28 15:13:01 : thread1 is finished.
2016-9-28 15:13:01 : thread1 is unloaded.
2016-9-28 15:13:01 : thread2 is locked.
2016-9-28 15:13:01 : thread2 doSoming1....
2016-9-28 15:13:01 : thread 1 end.
2016-9-28 15:13:06 : thread2 doSoming2....
2016-9-28 15:13:06 : thread2 is finished.
2016-9-28 15:13:06 : thread2 is unloaded.
2016-9-28 15:13:06 : thread 2 end.
可以看到,thread1先獲得鎖,一會(huì)thread2也來(lái)拿鎖,但這個(gè)時(shí)候thread1已經(jīng)占用了,所以thread2一直到thread1釋放了鎖后才拿到鎖。
**這段代碼說(shuō)明lockInterruptibly后面來(lái)獲取鎖的線程需要等待前面的鎖釋放了才能獲得鎖。**但這里還沒(méi)有體現(xiàn)出可中斷的特點(diǎn),為此增加一些代碼:
thread2.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //1秒后把線程2中斷 thread2.interrupt();
在thread2啟動(dòng)后調(diào)用一下thread2的中斷方法,好吧,先跑一下代碼看看結(jié)果:
2016-9-28 15:16:46 : thread 1 get lock.
2016-9-28 15:16:46 : thread1 is locked.
2016-9-28 15:16:46 : thread1 doSoming1....
2016-9-28 15:16:46 : thread 2 get lock.
2016-9-28 15:16:47 : thread 2 is interrupted. <--直接就響應(yīng)了線程中斷
2016-9-28 15:16:51 : thread1 doSoming2....
2016-9-28 15:16:51 : thread1 is finished.
2016-9-28 15:16:51 : thread1 is unloaded.
2016-9-28 15:16:51 : thread 1 end.
和前面的代碼相比可以發(fā)現(xiàn),thread2正在等待thread1釋放鎖,但是這時(shí)thread2自己中斷了,thread2后面的代碼則不會(huì)再繼續(xù)執(zhí)行。
ReadWriteLock
顧名思義就是讀寫(xiě)鎖,這種讀-寫(xiě)鎖的應(yīng)用場(chǎng)景可以這樣理解,比如一波數(shù)據(jù)大部分時(shí)候都是提供讀取的,而只有比較少量的寫(xiě)操作,那么如果用互斥鎖的話(huà)就會(huì)導(dǎo)致線程間的鎖競(jìng)爭(zhēng)。如果對(duì)于讀取的時(shí)候大家都可以讀,一旦要寫(xiě)入的時(shí)候就再將某個(gè)資源鎖住。這樣的變化就很好的解決了這個(gè)問(wèn)題,使的讀操作可以提高讀的性能,又不會(huì)影響寫(xiě)的操作。
一個(gè)資源可以被多個(gè)讀者訪問(wèn),或者被一個(gè)寫(xiě)者訪問(wèn),兩者不能同時(shí)進(jìn)行。
這是讀寫(xiě)鎖的抽象接口,定義一個(gè)讀鎖和一個(gè)寫(xiě)鎖。
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(); }
在JDK里有個(gè)ReentrantReadWriteLock實(shí)現(xiàn),就是可重入的讀-寫(xiě)鎖。ReentrantReadWriteLock可以構(gòu)造為公平的或者非公平的兩種類(lèi)型。如果在構(gòu)造時(shí)不顯式指定則會(huì)默認(rèn)的創(chuàng)建非公平鎖。在非公平鎖的模式下,線程訪問(wèn)的順序是不確定的,就是可以闖入;可以由寫(xiě)者降級(jí)為讀者,但是讀者不能升級(jí)為寫(xiě)者。
如果是公平鎖模式,那么選擇權(quán)交給等待時(shí)間最長(zhǎng)的線程,如果一個(gè)讀線程獲得鎖,此時(shí)一個(gè)寫(xiě)線程請(qǐng)求寫(xiě)入鎖,那么就不再接收讀鎖的獲取,直到寫(xiě)入操作完成。
簡(jiǎn)單的代碼分析 在ReentrantReadWriteLock里其實(shí)維護(hù)的是一個(gè)sync的鎖,只是看起來(lái)語(yǔ)義上像是一個(gè)讀鎖和寫(xiě)鎖。看一下它的構(gòu)造函數(shù):
public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } //讀鎖的構(gòu)造函數(shù) protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; } //寫(xiě)鎖的構(gòu)造函數(shù) protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; }
可以看到實(shí)際上讀/寫(xiě)鎖在構(gòu)造時(shí)都是引用的ReentrantReadWriteLock的sync鎖對(duì)象。而這個(gè)Sync類(lèi)是ReentrantReadWriteLock的一個(gè)內(nèi)部類(lèi)??傊x/寫(xiě)鎖都是通過(guò)Sync來(lái)完成的。它是如何來(lái)協(xié)作這兩者關(guān)系呢?
//讀鎖的加鎖方法 public void lock() { sync.acquireShared(1); } //寫(xiě)鎖的加鎖方法 public void lock() { sync.acquire(1); }
區(qū)別主要是讀鎖獲得的是共享鎖,而寫(xiě)鎖獲取的是獨(dú)占鎖。這里有個(gè)點(diǎn)可以提一下,就是ReentrantReadWriteLock為了保證可重入性,共享鎖和獨(dú)占鎖都必須支持持有計(jì)數(shù)和重入數(shù)。而ReentrantLock是使用state來(lái)存儲(chǔ)的,而state只能存一個(gè)整形值,為了兼容兩個(gè)鎖的問(wèn)題,所以將其劃分了高16位和低16位分別存共享鎖的線程數(shù)量或獨(dú)占鎖的線程數(shù)量或者重入計(jì)數(shù)。
其他
寫(xiě)了一大篇感覺(jué)要寫(xiě)下去篇幅太長(zhǎng)了,還有一些比較有用的鎖:
CountDownLatch
就是設(shè)置一個(gè)同時(shí)持有的計(jì)數(shù)器,而調(diào)用者調(diào)用CountDownLatch的await方法時(shí)如果當(dāng)前的計(jì)數(shù)器不為0就會(huì)阻塞,調(diào)用CountDownLatch的release方法可以減少計(jì)數(shù),直到計(jì)數(shù)為0時(shí)調(diào)用了await的調(diào)用者會(huì)解除阻塞。
Semaphone
信號(hào)量是一種通過(guò)授權(quán)許可的形式,比如設(shè)置100個(gè)許可證,這樣就可以同時(shí)有100個(gè)線程同時(shí)持有鎖,如果超過(guò)這個(gè)量后就會(huì)返回失敗。
感謝閱讀此文,希望能幫助到大家,謝謝大家對(duì)本站的支持!
相關(guān)文章
基于JAVA的短信驗(yàn)證碼api調(diào)用代碼實(shí)例
這篇文章主要為大家詳細(xì)介紹了基于JAVA的短信驗(yàn)證碼api調(diào)用代碼實(shí)例,感興趣的小伙伴們可以參考一下2016-05-05SpringBoot中使用configtree讀取樹(shù)形文件目錄中的配置詳解
這篇文章主要介紹了SpringBoot中使用configtree讀取樹(shù)形文件目錄中的配置詳解,configtree通過(guò)spring.config.import?+?configtree:前綴的方式,加載以文件名為key、文件內(nèi)容為value的配置屬性,需要的朋友可以參考下2023-12-12Mybatis關(guān)于動(dòng)態(tài)排序 #{} ${}問(wèn)題
這篇文章主要介紹了Mybatis關(guān)于動(dòng)態(tài)排序 #{} ${}問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10Java?Hibernate中一對(duì)多和多對(duì)多關(guān)系的映射方式
Hibernate是一種Java對(duì)象關(guān)系映射框架,支持一對(duì)多和多對(duì)多關(guān)系的映射。一對(duì)多關(guān)系可以使用集合屬性和單向/雙向關(guān)聯(lián)來(lái)映射,多對(duì)多關(guān)系可以使用集合屬性和中間表來(lái)映射。在映射過(guò)程中,需要注意級(jí)聯(lián)操作、延遲加載、中間表的處理等問(wèn)題2023-04-04RocketMQ?Broker實(shí)現(xiàn)高可用高并發(fā)的消息中轉(zhuǎn)服務(wù)
RocketMQ消息代理(Broker)是一種高可用、高并發(fā)的消息中轉(zhuǎn)服務(wù),能夠接收并存儲(chǔ)生產(chǎn)者發(fā)送的消息,并將消息發(fā)送給消費(fèi)者。它具有多種消息存儲(chǔ)模式和消息傳遞模式,支持水平擴(kuò)展和故障轉(zhuǎn)移等特性,可以為分布式應(yīng)用提供可靠的消息傳遞服務(wù)2023-04-04@ConfigurationProperties加載外部配置方式
這篇文章主要介紹了@ConfigurationProperties加載外部配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03java JDBC系列教程之JDBC類(lèi)的簡(jiǎn)析與JDBC的基礎(chǔ)操作
這篇文章主要介紹了java JDBC系列教程之JDBC類(lèi)的簡(jiǎn)析與JDBC的基礎(chǔ)操作,本文分步驟通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07