您好,登錄后才能下訂單哦!
昨天在學(xué)習(xí)別人分享的面試經(jīng)驗時,看到Lock的使用。想起自己在上次面試也遇到了synchronized與Lock的區(qū)別與使用。于是,我整理了兩者的區(qū)別和使用情況,同時,對synchronized的使用過程一些常見問題的總結(jié),最后是參照源碼和說明文檔,對Lock的使用寫了幾個簡單的Demo。請大家批評指正。
技術(shù)點:
1、線程與進(jìn)程:
在開始之前先把進(jìn)程與線程進(jìn)行區(qū)分一下,一個程序最少需要一個進(jìn)程,而一個進(jìn)程最少需要一個線程。關(guān)系是線程–>進(jìn)程–>程序的大致組成結(jié)構(gòu)。所以線程是程序執(zhí)行流的最小單位,而進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個獨立單位。以下我們所有討論的都是建立在線程基礎(chǔ)之上。
2、Thread的幾個重要方法:
我們先了解一下Thread的幾個重要方法。a、start()方法,調(diào)用該方法開始執(zhí)行該線程;b、stop()方法,調(diào)用該方法強(qiáng)制結(jié)束該線程執(zhí)行;c、join方法,調(diào)用該方法等待該線程結(jié)束。d、sleep()方法,調(diào)用該方法該線程進(jìn)入等待。e、run()方法,調(diào)用該方法直接執(zhí)行線程的run()方法,但是線程調(diào)用start()方法時也會運(yùn)行run()方法,區(qū)別就是一個是由線程調(diào)度運(yùn)行run()方法,一個是直接調(diào)用了線程中的run()方法?。?/p>
看到這里,可能有些人就會問啦,那wait()和notify()呢?要注意,其實wait()與notify()方法是Object的方法,不是Thread的方法!!同時,wait()與notify()會配合使用,分別表示線程掛起和線程恢復(fù)。
這里還有一個很常見的問題,順帶提一下:wait()與sleep()的區(qū)別,簡單來說wait()會釋放對象鎖而sleep()不會釋放對象鎖。這些問題有很多的資料,不再贅述。
3、線程狀態(tài):
線程總共有5大狀態(tài),通過上面第二個知識點的介紹,理解起來就簡單了。
新建狀態(tài):新建線程對象,并沒有調(diào)用start()方法之前
就緒狀態(tài):調(diào)用start()方法之后線程就進(jìn)入就緒狀態(tài),但是并不是說只要調(diào)用start()方法線程就馬上變?yōu)楫?dāng)前線程,在變?yōu)楫?dāng)前線程之前都是為就緒狀態(tài)。值得一提的是,線程在睡眠和掛起中恢復(fù)的時候也會進(jìn)入就緒狀態(tài)哦。
運(yùn)行狀態(tài):線程被設(shè)置為當(dāng)前線程,開始執(zhí)行run()方法。就是線程進(jìn)入運(yùn)行狀態(tài)
阻塞狀態(tài):線程被暫停,比如說調(diào)用sleep()方法后線程就進(jìn)入阻塞狀態(tài)
死亡狀態(tài):線程執(zhí)行結(jié)束
4、鎖類型
可重入鎖:在執(zhí)行對象中所有同步方法不用再次獲得鎖
可中斷鎖:在等待獲取鎖過程中可中斷
公平鎖: 按等待獲取鎖的線程的等待時間進(jìn)行獲取,等待時間長的具有優(yōu)先獲取鎖權(quán)利
讀寫鎖:對資源讀取和寫入的時候拆分為2部分處理,讀的時候可以多線程一起讀,寫的時候必須同步地寫
1、我把兩者的區(qū)別分類到了一個表中,方便大家對比:
類別 | synchronized | Lock |
---|---|---|
存在層次 | Java的關(guān)鍵字,在jvm層面上 | 是一個類 |
鎖的釋放 | 1、以獲取鎖的線程執(zhí)行完同步代碼,釋放鎖 2、線程執(zhí)行發(fā)生異常,jvm會讓線程釋放鎖 | 在finally中必須釋放鎖,不然容易造成線程死鎖 |
鎖的獲取 | 假設(shè)A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待 | 分情況而定,Lock有多個鎖獲取的方式,具體下面會說道,大致就是可以嘗試獲得鎖,線程可以不用一直等待 |
鎖狀態(tài) | 無法判斷 | 可以判斷 |
鎖類型 | 可重入 不可中斷 非公平 | 可重入 可判斷 可公平(兩者皆可) |
性能 | 少量同步 | 大量同步 |
或許,看到這里還對LOCK所知甚少,那么接下來,我們進(jìn)入LOCK的深入學(xué)習(xí)。
以下是Lock接口的源碼,筆者修剪之后的結(jié)果:
public interface Lock { /** * Acquires the lock. */ void lock(); /** * Acquires the lock unless the current thread is * {@linkplain Thread#interrupt interrupted}. */ void lockInterruptibly() throws InterruptedException; /** * Acquires the lock only if it is free at the time of invocation. */ boolean tryLock(); /** * Acquires the lock if it is free within the given waiting time and the * current thread has not been {@linkplain Thread#interrupt interrupted}. */ boolean tryLock(long time, TimeUnit unit) throws InterruptedException; /** * Releases the lock. */ void unlock(); }123456789101112131415161718192021222324252627282930
從Lock接口中我們可以看到主要有個方法,這些方法的功能從注釋中可以看出:12
lock():獲取鎖,如果鎖被暫用則一直等待
unlock():釋放鎖
tryLock(): 注意返回類型是boolean,如果獲取鎖的時候鎖被占用就返回false,否則返回true
tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待參數(shù)時間
lockInterruptibly():用該鎖的獲得方式,如果線程在獲取鎖的階段進(jìn)入了等待,那么可以中斷此線程,先去做別的事
通過 以上的解釋,大致可以解釋在上個部分中“鎖類型(lockInterruptibly())”,“鎖狀態(tài)(tryLock())”等問題,還有就是前面子所獲取的過程我所寫的“大致就是可以嘗試獲得鎖,線程可以不會一直等待”用了“可以”的原因。
下面是Lock一般使用的例子,注意ReentrantLock是Lock接口的實現(xiàn)。12
lock():
package com.brickworkers;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class LockTest { private Lock lock = new ReentrantLock(); //需要參與同步的方法 private void method(Thread thread){ lock.lock(); try { System.out.println("線程名"+thread.getName() + "獲得了鎖"); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名"+thread.getName() + "釋放了鎖"); lock.unlock(); } } public static void main(String[] args) { LockTest lockTest = new LockTest(); //線程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { lockTest.method(Thread.currentThread()); } }, "t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { lockTest.method(Thread.currentThread()); } }, "t2"); t1.start(); t2.start(); } }//執(zhí)行情況:線程名t1獲得了鎖// 線程名t1釋放了鎖// 線程名t2獲得了鎖// 線程名t2釋放了鎖1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
tryLock():
package com.brickworkers;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class LockTest { private Lock lock = new ReentrantLock(); //需要參與同步的方法 private void method(Thread thread){/* lock.lock(); try { System.out.println("線程名"+thread.getName() + "獲得了鎖"); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名"+thread.getName() + "釋放了鎖"); lock.unlock(); }*/ if(lock.tryLock()){ try { System.out.println("線程名"+thread.getName() + "獲得了鎖"); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名"+thread.getName() + "釋放了鎖"); lock.unlock(); } }else{ System.out.println("我是"+Thread.currentThread().getName()+"有人占著鎖,我就不要啦"); } } public static void main(String[] args) { LockTest lockTest = new LockTest(); //線程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { lockTest.method(Thread.currentThread()); } }, "t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { lockTest.method(Thread.currentThread()); } }, "t2"); t1.start(); t2.start(); } }//執(zhí)行結(jié)果: 線程名t2獲得了鎖// 我是t1有人占著鎖,我就不要啦// 線程名t2釋放了鎖12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
看到這里相信大家也都會使用如何使用Lock了吧,關(guān)于tryLock(long time, TimeUnit unit)和lockInterruptibly()不再贅述。前者主要存在一個等待時間,在測試代碼中寫入一個等待時間,后者主要是等待中斷,會拋出一個中斷異常,常用度不高,喜歡探究可以自己深入研究。
前面比較重提到“公平鎖”,在這里可以提一下ReentrantLock對于平衡鎖的定義,在源碼中有這么兩段:12
/** * Sync object for non-fair locks */ static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } /** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
從以上源碼可以看出在Lock中可以自己控制鎖是否公平,而且,默認(rèn)的是非公平鎖,以下是ReentrantLock的構(gòu)造函數(shù):
public ReentrantLock() { sync = new NonfairSync();//默認(rèn)非公平鎖 }123
筆者水平一般,不過此博客在引言中的目的已全部達(dá)到。這只是筆者在學(xué)習(xí)過程中的總結(jié)與概括,如存在不正確的,歡迎大家批評指出。12
延伸學(xué)習(xí):對于LOCK底層的實現(xiàn),大家可以參考:
點擊Lock底層介紹博客
兩種同步方式性能測試,大家可以參考:
點擊查看兩種同步方式性能測試博客
回來看自己博客。發(fā)現(xiàn)東西闡述的不夠完整。這里在做補(bǔ)充,因為這篇博客訪問較大,所以為了不誤導(dǎo)大家,盡量介紹給大家正確的表述:
1、兩種鎖的底層實現(xiàn)方式:
synchronized:我們知道java是用字節(jié)碼指令來控制程序(這里不包括熱點代碼編譯成機(jī)器碼)。在字節(jié)指令中,存在有synchronized所包含的代碼塊,那么會形成2段流程的執(zhí)行。
我們點擊查看SyncDemo.java的源碼SyncDemo.class,可以看到如下:
如上就是這段代碼段字節(jié)碼指令,沒你想的那么難吧。言歸正傳,我們可以清晰段看到,其實synchronized映射成字節(jié)碼指令就是增加來兩個指令:monitorenter和monitorexit。當(dāng)一條線程進(jìn)行執(zhí)行的遇到monitorenter指令的時候,它會去嘗試獲得鎖,如果獲得鎖那么鎖計數(shù)+1(為什么會加一呢,因為它是一個可重入鎖,所以需要用這個鎖計數(shù)判斷鎖的情況),如果沒有獲得鎖,那么阻塞。當(dāng)它遇到monitorexit的時候,鎖計數(shù)器-1,當(dāng)計數(shù)器為0,那么就釋放鎖。
那么有的朋友看到這里就疑惑了,那圖上有2個monitorexit呀?馬上回答這個問題:上面我以前寫的文章也有表述過,synchronized鎖釋放有兩種機(jī)制,一種就是執(zhí)行完釋放;另外一種就是發(fā)送異常,虛擬機(jī)釋放。圖中第二個monitorexit就是發(fā)生異常時執(zhí)行的流程,這就是我開頭說的“會有2個流程存在“。而且,從圖中我們也可以看到在第13行,有一個goto指令,也就是說如果正常運(yùn)行結(jié)束會跳轉(zhuǎn)到19行執(zhí)行。
這下,你對synchronized是不是了解的很清晰了呢。接下來我們再聊一聊Lock。
Lock:Lock實現(xiàn)和synchronized不一樣,后者是一種悲觀鎖,它膽子很小,它很怕有人和它搶吃的,所以它每次吃東西前都把自己關(guān)起來。而Lock呢底層其實是CAS樂觀鎖的體現(xiàn),它無所謂,別人搶了它吃的,它重新去拿吃的就好啦,所以它很樂觀。具體底層怎么實現(xiàn),博主不在細(xì)述,有機(jī)會的話,我會對concurrent包下面的機(jī)制好好和大家說說,如果面試問起,你就說底層主要靠volatile和CAS操作實現(xiàn)的。
現(xiàn)在,才是我真正想在這篇博文后面加的,我要說的是:盡可能去使用synchronized而不要去使用LOCK
什么概念呢?我和大家打個比方:你叫jdk,你生了一個孩子叫synchronized,后來呢,你領(lǐng)養(yǎng)了一個孩子叫LOCK。起初,LOCK剛來到新家的時候,它很乖,很懂事,各個方面都表現(xiàn)的比synchronized好。你很開心,但是你內(nèi)心深處又有一點淡淡的憂傷,你不希望你自己親生的孩子竟然還不如一個領(lǐng)養(yǎng)的孩子乖巧。這個時候,你對親生的孩子教育更加深刻了,你想證明,你的親生孩子synchronized并不會比領(lǐng)養(yǎng)的孩子LOCK差。(博主只是打個比方)
那如何教育呢?
在jdk1.6~jdk1.7的時候,也就是synchronized16、7歲的時候,你作為爸爸,你給他優(yōu)化了,具體優(yōu)化在哪里呢:
1、線程自旋和適應(yīng)性自旋
我們知道,java’線程其實是映射在內(nèi)核之上的,線程的掛起和恢復(fù)會極大的影響開銷。并且jdk官方人員發(fā)現(xiàn),很多線程在等待鎖的時候,在很短的一段時間就獲得了鎖,所以它們在線程等待的時候,并不需要把線程掛起,而是讓他無目的的循環(huán),一般設(shè)置10次。這樣就避免了線程切換的開銷,極大的提升了性能。
而適應(yīng)性自旋,是賦予了自旋一種學(xué)習(xí)能力,它并不固定自旋10次一下。他可以根據(jù)它前面線程的自旋情況,從而調(diào)整它的自旋,甚至是不經(jīng)過自旋而直接掛起。
2、鎖消除
什么叫鎖消除呢?就是把不必要的同步在編譯階段進(jìn)行移除。
那么有的小伙伴又迷糊了,我自己寫的代碼我會不知道這里要不要加鎖?我加了鎖就是表示這邊會有同步呀?
并不是這樣,這里所說的鎖消除并不一定指代是你寫的代碼的鎖消除,我打一個比方:
在jdk1.5以前,我們的String字符串拼接操作其實底層是StringBuffer來實現(xiàn)的(這個大家可以用我前面介紹的方法,寫一個簡單的demo,然后查看class文件中的字節(jié)碼指令就清楚了),而在jdk1.5之后,那么是用StringBuilder來拼接的。我們考慮前面的情況,比如如下代碼:
String str1="qwe"; String str2="asd"; String str3=str1+str2;123
底層實現(xiàn)會變成這樣:
StringBuffer sb = new StringBuffer(); sb.append("qwe"); sb.append("asd");123
我們知道,StringBuffer是一個線程安全的類,也就是說兩個append方法都會同步,通過指針逃逸分析(就是變量不會外泄),我們發(fā)現(xiàn)在這段代碼并不存在線程安全問題,這個時候就會把這個同步鎖消除。
3、鎖粗化
在用synchronized的時候,我們都講究為了避免大開銷,盡量同步代碼塊要小。那么為什么還要加粗呢?
我們繼續(xù)以上面的字符串拼接為例,我們知道在這一段代碼中,每一個append都需要同步一次,那么我可以把鎖粗化到第一個append和最后一個append(這里不要去糾結(jié)前面的鎖消除,我只是打個比方)
4、輕量級鎖
5、偏向鎖
關(guān)于最后這兩種,我希望留個有緣的讀者自己去查找,我不希望我把一件事情描述的那么詳細(xì),自己動手得到才是你自己的,博主可以告訴你的是,最后兩種并不難。。加油吧,各位。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。