synchronized的功能擴展:重入鎖
重入鎖可以完全替代synchronized關(guān)鍵字。在JDK 5.0的早期版本中,重入鎖的性能遠遠好于synchronized藏鹊,但從JDK 6.0開始轨帜,JDK在synchronized上做了大量的優(yōu)化,使得兩者的性能差距并不大鞋拟。
重入鎖使用java.util.concurrent.locks.ReentrantLock類來實現(xiàn)骂维。
重入鎖使用案例
01 public class ReenterLock implements Runnable{
02 public static ReentrantLock lock=new ReentrantLock();
03 public static int i=0;
04 @Override
05 public void run() {
06 for(int j=0;j<10000000;j++){
07 lock.lock();
08 try{
09 i++;
10 }finally{
11 lock.unlock();
12 }
13 }
14 }
15 public static void main(String[] args) throws InterruptedException {
16 ReenterLock tl=new ReenterLock();
17 Thread t1=new Thread(tl);
18 Thread t2=new Thread(tl);
19 t1.start();t2.start();
20 t1.join();t2.join();
21 System.out.println(i);
22 }
23 }
7~12行,使用重入鎖保護臨界區(qū)資源i贺纲,確保多線程對i操作的安全性航闺。重入鎖與synchronized相比,重入鎖有著顯示的操作過程。開發(fā)人員必須手動指定何時加鎖潦刃,何時釋放鎖侮措。也正因為這樣,重入鎖對邏輯控制的靈活性要遠遠好于synchronized乖杠。但值得注意的是萝毛,在退出臨界區(qū)時,必須記得釋放鎖(代碼第11行)滑黔,否則笆包,其他線程就沒有機會再訪問臨界區(qū)了。
重點
重入鎖的特點
獲取鎖的線程略荡,可以重復(fù)獲取該鎖庵佣,稱之為重入鎖。
比如上面代碼改成如下更能體現(xiàn)出來重入鎖的特點
lock.lock();
lock.lock();
try{
i++;
}finally{
lock.unlock();
lock.unlock();
}
在這種情況下汛兜,一個線程連續(xù)兩次獲得同一把鎖巴粪。這是允許的!如果不允許這么操作粥谬,那么同一個線程在第2次獲得鎖時肛根,將會和自己產(chǎn)生死鎖。程序就會“卡死”在第2次申請鎖的過程中漏策。但需要注意的是派哲,如果同一個線程多次獲得鎖,那么在釋放鎖的時候掺喻,也必須釋放相同次數(shù)芭届。如果釋放鎖的次數(shù)多,那么會得到一個java.lang.IllegalMonitorStateException異常感耙,反之褂乍,如果釋放鎖的次數(shù)少了,那么相當(dāng)于線程還持有這個鎖即硼,因此逃片,其他線程也無法進入臨界區(qū)。只酥、
中斷響應(yīng)
對于synchronized來說褥实,如果一個線程在等待鎖,那么結(jié)果只有兩種情況层皱,要么它獲得這把鎖繼續(xù)執(zhí)行性锭,要么它就保持等待。而使用重入鎖叫胖,則提供另外一種可能草冈,那就是線程可以被中斷。
中斷響應(yīng)案例
01 public class IntLock implements Runnable {
02 public static ReentrantLock lock1 = new ReentrantLock();
03 public static ReentrantLock lock2 = new ReentrantLock();
04 int lock;
05 /**
06 * 控制加鎖順序,方便構(gòu)造死鎖
07 * @param lock
08 */
09 public IntLock(int lock) {
10 this.lock = lock;
11 }
12
13 @Override
14 public void run() {
15 try {
16 if (lock == 1) {
17 lock1.lockInterruptibly();
18 try{
19 Thread.sleep(500);
20 }catch(InterruptedException e){}
21 lock2.lockInterruptibly();
22 } else {
23 lock2.lockInterruptibly();
24 try{
25 Thread.sleep(500);
26 }catch(InterruptedException e){}
27 lock1.lockInterruptibly();
28 }
29
30 } catch (InterruptedException e) {
31 e.printStackTrace();
32 } finally {
33 if (lock1.isHeldByCurrentThread())
34 lock1.unlock();
35 if (lock2.isHeldByCurrentThread())
36 lock2.unlock();
37 System.out.println(Thread.currentThread().getId()+":線程退出");
38 }
39 }
40
41 public static void main(String[] args) throws InterruptedException {
42 IntLock r1 = new IntLock(1);
43 IntLock r2 = new IntLock(2);
44 Thread t1 = new Thread(r1);
45 Thread t2 = new Thread(r2);
46 t1.start();t2.start();
47 Thread.sleep(1000);
48 //中斷其中一個線程
49 t2.interrupt();
50 }
51 }
線程t1和t2啟動后怎棱,t1先占用lock1哩俭,再占用lock2;t2先占用lock2拳恋,再請求lock1凡资。因此,很容易形成t1和t2之間的相互等待谬运。在這里隙赁,對鎖的請求,統(tǒng)一使用lockInterruptibly()方法梆暖。這是一個可以對中斷進行響應(yīng)的鎖申請動作伞访,即在等待鎖的過程中,可以響應(yīng)中斷轰驳。
在代碼第47行厚掷,主線程main處于休眠,此時级解,這兩個線程處于死鎖的狀態(tài)冒黑,在代碼第49行,由于t2線程被中斷勤哗,故t2會放棄對lock1的申請抡爹,同時釋放已獲得lock2。這個操作導(dǎo)致t1線程可以順利得到lock2而繼續(xù)執(zhí)行下去俺陋。
另外上面通過加鎖順序豁延,構(gòu)造死鎖的思路也值得借鑒昙篙。
執(zhí)行上述代碼腊状,將輸出:
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.
doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.
acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly
(ReentrantLock.java:335)
at geym.conc.ch3.synctrl.IntLock.run(IntLock.java:31)
at java.lang.Thread.run(Thread.java:745)
9:線程退出
8:線程退出
可以看到,中斷后苔可,兩個線程雙雙退出缴挖。但真正完成工作的只有t1。而t2線程則放棄其任務(wù)直接退出焚辅,釋放資源映屋。
重點
lock()與lockInterruptibly()的區(qū)別
ReentrantLock.lockInterruptibly允許在等待時由其它線程調(diào)用等待線程的Thread.interrupt方法來中斷等待線程的等待而直接返回,這時不用獲取鎖同蜻,而會拋出一個InterruptedException棚点。
ReentrantLock.lock方法不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted,一樣會繼續(xù)嘗試獲取鎖,失敗則繼續(xù)休眠湾蔓。只是在最后獲取鎖成功后再把當(dāng)前線程置為interrupted狀態(tài),然后再中斷線程瘫析。
也就是只有通過lockInterruptibly()獲取鎖,才可以通過線程中斷。
interrupt()不提倡
這種中斷線程的方式并不提倡贬循,后面會解釋咸包。
鎖申請等待限時
除了等待外部通知之外,要避免死鎖還有另外一種方法杖虾,那就是限時等待烂瘫。
限時等待案例
01 public class TimeLock implements Runnable{
02 public static ReentrantLock lock=new ReentrantLock();
03 @Override
04 public void run() {
05 try {
06 if(lock.tryLock(5, TimeUnit.SECONDS)){
07 Thread.sleep(6000);
08 }else{
09 System.out.println("get lock failed");
10 }
11 } catch (InterruptedException e) {
12 e.printStackTrace();
13 }finally{if(lock.isHeldByCurrentThread()) lock.unlock();}
14 }
15 public static void main(String[] args) {
16 TimeLock tl=new TimeLock();
17 Thread t1=new Thread(tl);
18 Thread t2=new Thread(tl);
19 t1.start();
20 t2.start();
21 }
22 }
在這里,tryLock()方法接收兩個參數(shù)奇适,一個表示等待時長坟比,另外一個表示計時單位。這里的單位設(shè)置為秒嚷往,時長為5温算,表示線程在這個鎖請求中,最多等待5秒间影。如果超過5秒還沒有得到鎖注竿,就會返回false。如果成功獲得鎖魂贬,則返回true巩割。
在本例中,由于占用鎖的線程會持有鎖長達6秒付燥,故另一個線程無法在5秒的等待時間內(nèi)獲得鎖宣谈,因此,請求鎖會失敗键科。
ReentrantLock.tryLock()方法也可以不帶參數(shù)直接運行闻丑。在這種情況下,當(dāng)前線程會嘗試獲得鎖勋颖,如果鎖并未被其他線程占用嗦嗡,則申請鎖會成功,并立即返回true饭玲。如果鎖被其他線程占用侥祭,則當(dāng)前線程不會進行等待,而是立即返回false茄厘。這種模式不會引起線程等待矮冬,因此也不會產(chǎn)生死鎖。下面演示了這種使用方式:
01 public class TryLock implements Runnable {
02 public static ReentrantLock lock1 = new ReentrantLock();
03 public static ReentrantLock lock2 = new ReentrantLock();
04 int lock;
05
06 public TryLock(int lock) {
07 this.lock = lock;
08 }
09
10 @Override
11 public void run() {
12 if (lock == 1) {
13 while (true) {
14 if (lock1.tryLock()) {
15 try {
16 try {
17 Thread.sleep(500);
18 } catch (InterruptedException e) {
19 }
20 if (lock2.tryLock()) {
21 try {
22 System.out.println(Thread.currentThread()
23 .getId() + ":My Job done");
24 return;
25 } finally {
26 lock2.unlock();
27 }
28 }
29 } finally {
30 lock1.unlock();
31 }
32 }
33 }
34 } else {
35 while (true) {
36 if (lock2.tryLock()) {
37 try {
38 try {
39 Thread.sleep(500);
40 } catch (InterruptedException e) {
41 }
42 if (lock1.tryLock()) {
43 try {
44 System.out.println(Thread.currentThread()
45 .getId() + ":My Job done");
46 return;
47 } finally {
48 lock1.unlock();
49 }
50 }
51 } finally {
52 lock2.unlock();
53 }
54 }
55 }
56 }
57 }
58
59 public static void main(String[] args) throws InterruptedException {
60 TryLock r1 = new TryLock(1);
61 TryLock r2 = new TryLock(2);
62 Thread t1 = new Thread(r1);
63 Thread t2 = new Thread(r2);
64 t1.start();
65 t2.start();
66 }
67 }
輸出
9:My Job done
8:My Job done
上述代碼中次哈,采用了非常容易死鎖的加鎖順序胎署。也就是先讓t1獲得lock1,再讓t2獲得lock2窑滞,接著做反向請求琼牧,讓t1申請lock2径筏,t2申請lock1。在一般情況下障陶,這會導(dǎo)致t1和t2相互等待滋恬,從而引起死鎖。
但是使用tryLock()后抱究,這種情況就大大改善了融欧。由于線程不會傻傻地等待索守,而是不停地嘗試,因此,只要執(zhí)行足夠長的時間友鼻,線程總是會得到所有需要的資源高镐,從而正常執(zhí)行(這里以線程同時獲得lock1和lock2兩把鎖埠忘,作為其可以正常執(zhí)行的條件)立哑。在同時獲得lock1和lock2后,線程就打印出標(biāo)志著任務(wù)完成的信息“My Job done”苦银。
上面代碼獲取到鎖的時機是:一旦有一個線程釋放所有資源啸胧,此時剛結(jié)束sleep狀態(tài)的線程就可以獲取到鎖,即同時獲取兩把鎖幔虏,結(jié)束線程從而釋放所有鎖纺念,此時另一個線程因為沒有了競爭,也可以獲取鎖想括,從而結(jié)束線程陷谱。
公平鎖
在大多數(shù)情況下,鎖的申請都是非公平的瑟蜈。非公平鎖可能會引發(fā)線程饑餓現(xiàn)象烟逊,即線程隨機獲取鎖,但是有的線程總也獲取不到鎖铺根。
公平鎖的一大特點是:它不會產(chǎn)生饑餓現(xiàn)象宪躯。只要你排隊,最終還是可以等到資源的夷都。如果我們使用synchronized關(guān)鍵字進行鎖控制眷唉,那么產(chǎn)生的鎖就是非公平的。而重入鎖允許我們對其公平性進行設(shè)置囤官。它有一個如下的構(gòu)造函數(shù):
public ReentrantLock(boolean fair)
當(dāng)參數(shù)fair為true時,表示鎖是公平的蛤虐。公平鎖看起來很優(yōu)美党饮,但是要實現(xiàn)公平鎖必然要求系統(tǒng)維護一個有序隊列,因此公平鎖的實現(xiàn)成本比較高驳庭,性能相對也非常低下刑顺,因此氯窍,默認情況下,鎖是非公平的蹲堂。如果沒有特別的需求狼讨,也不需要使用公平鎖。公平鎖和非公平鎖在線程調(diào)度表現(xiàn)上也是非常不一樣的柒竞。下面的代碼可以很好地突出公平鎖的特點:
01 public class FairLock implements Runnable {
02 public static ReentrantLock fairLock = new ReentrantLock(true);
03
04 @Override
05 public void run() {
06 while(true){
07 try{
08 fairLock.lock();
09 System.out.println(Thread.currentThread().getName()+" 獲得鎖");
10 }finally{
11 fairLock.unlock();
12 }
13 }
14 }
15
16 public static void main(String[] args) throws InterruptedException {
17 FairLock r1 = new FairLock();
18 Thread t1=new Thread(r1,"Thread_t1");
19 Thread t2=new Thread(r1,"Thread_t2");
20 t1.start();t2.start();
21 }
22 }
上述代碼第2行政供,指定鎖是公平的。接著朽基,由兩個線程t1和t2分別請求這把鎖布隔,并且在得到鎖后,進行一個控制臺的輸出稼虎,表示自己得到了鎖衅檀。在公平鎖的情況下,得到輸出通常如下所示:
Thread_t1 獲得鎖
Thread_t2 獲得鎖
Thread_t1 獲得鎖
Thread_t2 獲得鎖
Thread_t1 獲得鎖
Thread_t2 獲得鎖
Thread_t1 獲得鎖
Thread_t2 獲得鎖
Thread_t1 獲得鎖
公平鎖中內(nèi)部維護了一個隊列霎俩,所有申請鎖的線程按照申請順序進入隊列哀军,按照入隊列的順序獲取鎖。
如果不使用公平鎖打却,那么情況會完全不一樣排苍,下面是使用非公平鎖時的部分輸出:
前面還有一大段t1連續(xù)獲得鎖的輸出
Thread_t1 獲得鎖
Thread_t1 獲得鎖
Thread_t1 獲得鎖
Thread_t1 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
后面還有一大段t2連續(xù)獲得鎖的輸出
可以看到,根據(jù)系統(tǒng)的調(diào)度学密,一個線程會傾向于再次獲取已經(jīng)持有的鎖淘衙,這種分配方式是高效的,但是無公平性可言腻暮。
ReentrantLock重要api整理
lock():獲得鎖彤守,如果鎖已經(jīng)被占用,則等待哭靖。
lockInterruptibly():獲得鎖具垫,但優(yōu)先響應(yīng)中斷。
tryLock():嘗試獲得鎖试幽,如果成功筝蚕,返回true,失敗返回false铺坞。該方法不等待起宽,立即返回。
tryLock(long time, TimeUnit unit):在給定時間內(nèi)嘗試獲得鎖济榨。
unlock():釋放鎖坯沪。
在重入鎖的實現(xiàn)中,主要包含三個要素:
第一擒滑,是原子狀態(tài)腐晾。原子狀態(tài)使用CAS操作來存儲當(dāng)前鎖的狀態(tài)叉弦,判斷鎖是否已經(jīng)被別的線程持有。
第二藻糖,是等待隊列淹冰。所有沒有請求到鎖的線程,會進入等待隊列進行等待巨柒。待有線程釋放鎖后樱拴,系統(tǒng)就能從等待隊列中喚醒一個線程,繼續(xù)工作潘拱。
第三疹鳄,是阻塞原語park()和unpark(),用來掛起和恢復(fù)線程芦岂。沒有得到鎖的線程將會被掛起瘪弓。
重入鎖的好搭檔:Condition條件
Condtion是與重入鎖相關(guān)聯(lián)的。通過Lock接口(重入鎖就實現(xiàn)了這一接口)的Condition newCondition()方法可以生成一個與當(dāng)前重入鎖綁定的Condition實例禽最。利用Condition對象腺怯,我們就可以讓線程在合適的時間等待,或者在某一個特定的時刻得到通知川无,繼續(xù)執(zhí)行呛占。
Condition接口提供的基本方法
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
- await()方法會使當(dāng)前線程等待,同時釋放當(dāng)前鎖懦趋,當(dāng)其他線程中使用signal()或者signalAll()方法時晾虑,線程會重新獲得鎖并繼續(xù)執(zhí)行〗鼋校或者當(dāng)線程被中斷時帜篇,也能跳出等待。這和Object.wait()方法很相似诫咱。
- awaitUninterruptibly()方法與await()方法基本相同笙隙,但是它并不會在等待過程中響應(yīng)中斷。
- singal()方法用于喚醒一個在等待中的線程坎缭。相對的singalAll()方法會喚醒所有在等待中的線程竟痰。這和Obejct.notify()方法很類似。
Condition案例
01 public class ReenterLockCondition implements Runnable{
02 public static ReentrantLock lock=new ReentrantLock();
03 public static Condition condition = lock.newCondition();
04 @Override
05 public void run() {
06 try {
07 lock.lock();
08 condition.await();
09 System.out.println("Thread is going on");
10 } catch (InterruptedException e) {
11 e.printStackTrace();
12 }finally{
13 lock.unlock();
14 }
15 }
16 public static void main(String[] args) throws InterruptedException {
17 ReenterLockCondition tl=new ReenterLockCondition();
18 Thread t1=new Thread(tl);
19 t1.start();
20 Thread.sleep(2000);
21 //通知線程t1繼續(xù)執(zhí)行
22 lock.lock();
23 condition.signal();
24 lock.unlock();
25 }
26 }
允許多個線程同時訪問:信號量
號量為多線程協(xié)作提供了更為強大的控制方法掏呼。廣義上說坏快,信號量是對鎖的擴展。無論是內(nèi)部鎖synchronized還是重入鎖ReentrantLock哄尔,一次都只允許一個線程訪問一個資源假消,而信號量卻可以指定多個線程,同時訪問某一個資源岭接。信號量主要提供了以下構(gòu)造函數(shù):
public Semaphore(int permits)
public Semaphore(int permits, boolean fair) //第二個參數(shù)可以指定是否公平
在構(gòu)造信號量對象時富拗,必須要指定信號量的準(zhǔn)入數(shù),即同時能申請多少個許可鸣戴。當(dāng)每個線程每次只申請一個許可時啃沪,這就相當(dāng)于指定了同時有多少個線程可以訪問某一個資源。
信號量的主要邏輯方法有:
public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release()
acquire()方法嘗試獲得一個準(zhǔn)入的許可窄锅。若無法獲得创千,則線程會等待,直到有線程釋放一個許可或者當(dāng)前線程被中斷入偷。acquireUninterruptibly()方法和acquire()方法類似追驴,但是不響應(yīng)中斷。tryAcquire()嘗試獲得一個許可疏之,如果成功返回true殿雪,失敗則返回false,它不會進行等待锋爪,立即返回丙曙。release()用于在線程訪問資源結(jié)束后,釋放一個許可其骄,以使其他等待許可的線程可以進行資源訪問亏镰。
案例
01 public class SemapDemo implements Runnable{
02 final Semaphore semp = new Semaphore(5);
03 @Override
04 public void run() {
05 try {
06 semp.acquire();
07 //模擬耗時操作
08 Thread.sleep(2000);
09 System.out.println(Thread.currentThread().getId()+":done!");
10 semp.release();
11 } catch (InterruptedException e) {
12 e.printStackTrace();
13 }
14 }
15
16 public static void main(String[] args) {
17 ExecutorService exec = Executors.newFixedThreadPool(20);
18 final SemapDemo demo=new SemapDemo();
19 for(int i=0;i<20;i++){
20 exec.submit(demo);
21 }
22 }
23 }
上述代碼中,第7~9行為臨界區(qū)管理代碼拯爽,程序會限制執(zhí)行這段代碼的線程數(shù)索抓。這里在第2行,申明了一個包含5個許可的信號量毯炮。這就意味著同時可以有5個線程進入代碼段第7~9行逼肯。申請信號量使用acquire()操作,在離開時否副,務(wù)必使用release()釋放信號量(代碼第10行)汉矿。這就和釋放鎖是一個道理。如果不幸發(fā)生了信號量的泄露(申請了但沒有釋放)备禀,那么可以進入臨界區(qū)的線程數(shù)量就會越來越少洲拇,直到所有的線程均不可訪問。在本例中曲尸,同時開啟20個線程赋续。觀察這段程序的輸出,你就會發(fā)現(xiàn)系統(tǒng)以5個線程一組為單位另患,依次輸出帶有線程ID的提示文本纽乱。
Semaphore 經(jīng)常用來做限流。
ReadWriteLock讀寫鎖
ReadWriteLock是JDK5中提供的讀寫分離鎖昆箕。讀寫分離鎖可以有效地幫助減少鎖競爭鸦列,以提升系統(tǒng)性能租冠。用鎖分離的機制來提升性能非常容易理解。
讀寫鎖特性
- 讀-讀不互斥:讀讀之間不阻塞薯嗤。
- 讀-寫互斥:讀阻塞寫顽爹,寫也會阻塞讀。
- 寫-寫互斥:寫寫阻塞骆姐。
案例
如果在系統(tǒng)中镜粤,讀操作次數(shù)遠遠大于寫操作,則讀寫鎖就可以發(fā)揮最大的功效玻褪,提升系統(tǒng)的性能肉渴。這里我給出一個稍微夸張點的案例,來說明讀寫鎖對性能的幫助带射。
01 public class ReadWriteLockDemo {
02 private static Lock lock=new ReentrantLock();
03 private static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
04 private static Lock readLock = readWriteLock.readLock();
05 private static Lock writeLock = readWriteLock.writeLock();
06 private int value;
07
08 public Object handleRead(Lock lock) throws InterruptedException{
09 try{
10 lock.lock(); //模擬讀操作
11 Thread.sleep(1000); //讀操作的耗時越多同规,讀寫鎖的優(yōu)勢就越明顯
12 return value;
13 }finally{
14 lock.unlock();
15 }
16 }
17
18 public void handleWrite(Lock lock,int index) throws InterruptedException{
19 try{
20 lock.lock(); //模擬寫操作
21 Thread.sleep(1000);
22 value=index;
23 }finally{
24 lock.unlock();
25 }
26 }
27
28 public static void main(String[] args) {
29 final ReadWriteLockDemo demo=new ReadWriteLockDemo();
30 Runnable readRunnale=new Runnable() {
31 @Override
32 public void run() {
33 try {
34 demo.handleRead(readLock);
35 // demo.handleRead(lock);
36 } catch (InterruptedException e) {
37 e.printStackTrace();
38 }
39 }
40 };
41 Runnable writeRunnale=new Runnable() {
42 @Override
43 public void run() {
44 try {
45 demo.handleWrite(writeLock,new Random().nextInt());
46 // demo.handleWrite(lock,new Random().nextInt());
47 } catch (InterruptedException e) {
48 e.printStackTrace();
49 }
50 }
51 };
52
53 for(int i=0;i<18;i++){
54 new Thread(readRunnale).start();
55 }
56
57 for(int i=18;i<20;i++){
58 new Thread(writeRunnale).start();
59 }
60 }
61 }
上述代碼中,第11行和第21行分別模擬了一個非常耗時的操作庸诱,讓線程耗時1秒鐘捻浦。它們分別對應(yīng)讀耗時和寫耗時。代碼第34和45行桥爽,分別是讀線程和寫線程朱灿。在這里,第34行使用讀鎖钠四,第35行使用寫鎖盗扒。第53~55行開啟18個讀線程,第57~59行缀去,開啟兩個寫線程侣灶。由于這里使用了讀寫分離,因此缕碎,<font color=red>讀線程完全并行褥影,而寫會阻塞讀,因此咏雌,實際上這段代碼運行大約2秒多就能結(jié)束(寫線程之間是實際串行的)凡怎。而如果使用第35行代替第34行,使用第46行代替第45行執(zhí)行上述代碼赊抖,即统倒,使用普通的重入鎖代替讀寫鎖。那么所有的讀和寫線程之間都必須相互等待氛雪,因此整個程序的執(zhí)行時間將長達20余秒房匆。</font>
倒計時器:CountDownLatch
CountDownLatch是一個非常實用的多線程控制工具類。
CountDownLatch的構(gòu)造函數(shù)接收一個整數(shù)作為參數(shù),即當(dāng)前這個計數(shù)器的計數(shù)個數(shù)浴鸿。
public CountDownLatch(int count)
案例
01 public class CountDownLatchDemo implements Runnable {
02 static final CountDownLatch end = new CountDownLatch(10);
03 static final CountDownLatchDemo demo=new CountDownLatchDemo();
04 @Override
05 public void run() {
06 try {
07 //模擬檢查任務(wù)
08 Thread.sleep(new Random().nextInt(10)*1000);
09 System.out.println("check complete");
10 end.countDown();
11 } catch (InterruptedException e) {
12 e.printStackTrace();
13 }
14 }
15 public static void main(String[] args) throws InterruptedException {
16 ExecutorService exec = Executors.newFixedThreadPool(10);
17 for(int i=0;i<10;i++){
18 exec.submit(demo);
19 }
20 //等待檢查
21 end.await();
22 //發(fā)射火箭
23 System.out.println("Fire!");
24 exec.shutdown();
25 }
26 }
述代碼第2行井氢,生成一個CountDownLatch實例。計數(shù)數(shù)量為10赚楚。這表示需要有10個線程完成任務(wù)毙沾,等待在CountDownLatch上的線程才能繼續(xù)執(zhí)行骗卜。代碼第10行宠页,使用了CountDownLatch.countdown()方法,也就是通知CountDownLatch寇仓,一個線程已經(jīng)完成了任務(wù)举户,倒計時器可以減1啦。第21行遍烦,使用CountDownLatch.await()方法俭嘁,要求主線程等待所有10個檢查任務(wù)全部完成。待10個任務(wù)全部完成后服猪,主線程才能繼續(xù)執(zhí)行供填。
主線程在CountDownLatch上等待,當(dāng)所有檢查任務(wù)全部完成后罢猪,主線程方能繼續(xù)執(zhí)行近她。
循環(huán)柵欄:CyclicBarrier
和CountDownLatch非常類似,它也可以實現(xiàn)線程間的計數(shù)等待膳帕,但它的功能比CountDownLatch更加復(fù)雜且強大粘捎。
CyclicBarrier和CountDownLatch的區(qū)別在于:
CyclicBarrier可以循環(huán)使用,而CountDownLatch只能使用一次危彩。
比CountDownLatch略微強大一些攒磨,CyclicBarrier可以接收一個參數(shù)作為barrierAction。所謂barrierAction就是當(dāng)計數(shù)器一次計數(shù)完成后汤徽,系統(tǒng)會執(zhí)行的動作娩缰。如下構(gòu)造函數(shù),其中谒府,parties表示計數(shù)總數(shù)拼坎,也就是參與的線程總數(shù)。
public CyclicBarrier(int parties, Runnable barrierAction)
案例
01 public class CyclicBarrierDemo {
02 public static class Soldier implements Runnable {
03 private String soldier;
04 private final CyclicBarrier cyclic;
05
06 Soldier(CyclicBarrier cyclic, String soldierName) {
07 this.cyclic = cyclic;
08 this.soldier = soldierName;
09 }
10
11 public void run() {
12 try {
13 //等待所有士兵到齊
14 cyclic.await();
15 doWork();
16 //等待所有士兵完成工作
17 cyclic.await();
18 } catch (InterruptedException e) {
19 e.printStackTrace();
20 } catch (BrokenBarrierException e) {
21 e.printStackTrace();
22 }
23 }
24
25 void doWork() {
26 try {
27 Thread.sleep(Math.abs(new Random().nextInt()%10000));
28 } catch (InterruptedException e) {
29 e.printStackTrace();
30 }
31 System.out.println(soldier + ":任務(wù)完成");
32 }
33 }
34
35 public static class BarrierRun implements Runnable {
36 boolean flag;
37 int N;
38 public BarrierRun(boolean flag, int N) {
39 this.flag = flag;
40 this.N = N;
41 }
42
43 public void run() {
44 if (flag) {
45 System.out.println("司令:[士兵" + N + "個狱掂,任務(wù)完成演痒!]");
46 } else {
47 System.out.println("司令:[士兵" + N + "個,集合完畢趋惨!]");
48 flag = true;
49 }
50 }
51 }
52
53 public static void main(String args[]) throws InterruptedException {
54 final int N = 10;
55 Thread[] allSoldier=new Thread[N];
56 boolean flag = false;
57 CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N));
58 //設(shè)置屏障點鸟顺,主要是為了執(zhí)行這個方法
59 System.out.println("集合隊伍!");
60 for (int i = 0; i < N; ++i) {
61 System.out.println("士兵 "+i+" 報道!");
62 allSoldier[i]=new Thread(new Soldier(cyclic, "士兵 " + i));
63 allSoldier[i].start();
64 }
65 }
66 }
輸出
集合隊伍讯嫂!
士兵 0 報道蹦锋!
//篇幅有限,省略其他幾個士兵
士兵 9 報道欧芽!
司令:[士兵10個莉掂,集合完畢!]
士兵 0:任務(wù)完成
//篇幅有限千扔,省略其他幾個士兵
士兵 4:任務(wù)完成
司令:[士兵10個憎妙,任務(wù)完成!]
通過上面案例可知曲楚,CyclicBarrier可以循環(huán)使用厘唾。
程阻塞工具類:LockSupport
LockSupport的靜態(tài)方法park()可以阻塞當(dāng)前線程,類似的還有parkNanos()龙誊、parkUntil()等方法抚垃。它們實現(xiàn)了一個限時的等待。
01 public class LockSupportDemo {
02 public static Object u = new Object();
03 static ChangeObjectThread t1 = new ChangeObjectThread("t1");
04 static ChangeObjectThread t2 = new ChangeObjectThread("t2");
05
06 public static class ChangeObjectThread extends Thread {
07 public ChangeObjectThread(String name){
08 super.setName(name);
09 }
10 @Override
11 public void run() {
12 synchronized (u) {
13 System.out.println("in "+getName());
14 LockSupport.park();
15 }
16 }
17 }
18
19 public static void main(String[] args) throws InterruptedException {
20 t1.start();
21 Thread.sleep(100);
22 t2.start();
23 LockSupport.unpark(t1);
24 LockSupport.unpark(t2);
25 t1.join();
26 t2.join();
27 }
28 }
按照以前的思想趟大,上面代碼23行unpark不一定發(fā)生在14行park后鹤树,也就有幾率導(dǎo)致線程永久掛起。但是執(zhí)行這段代碼逊朽,你會發(fā)現(xiàn)罕伯,它自始至終都可以正常的結(jié)束,不會因為park()方法而導(dǎo)致線程永久性的掛起惋耙。
這是因為LockSupport類使用類似信號量的機制捣炬。它為每一個線程準(zhǔn)備了一個許可,如果許可可用绽榛,那么park()函數(shù)會立即返回湿酸,并且消費這個許可(也就是將許可變?yōu)椴豢捎茫绻S可不可用灭美,就會阻塞推溃。而unpark()則使得一個許可變?yōu)榭捎茫ǖ呛托盘柫坎煌氖牵S可不能累加届腐,你不可能擁有超過一個許可铁坎,它永遠只有一個)。
這個特點使得:即使<font color=red>unpark()操作發(fā)生在park()之前犁苏,它也可以使下一次的park()操作立即返回硬萍。這也就是上述代碼可順利結(jié)束的主要原因</font>。
同時围详,處于park()掛起狀態(tài)的線程不會像suspend()那樣還給出一個令人費解的Runnable的狀態(tài)朴乖。它會非常明確地給出一個WAITING狀態(tài)祖屏,甚至還會標(biāo)注是park()引起的:
"t1" #8 prio=5 os_prio=0 tid=0x00b1a400 nid=0x1994 waiting on condition [0x1619f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
at geym.conc.ch3.ls.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java:18)
- locked <0x048b2680> (a java.lang.Object)
使得分析問題時格外方便。此外买羞,如果你使用park(Object)函數(shù)袁勺,還可以為當(dāng)前線程設(shè)置一個阻塞對象。這個阻塞對象會出現(xiàn)在線程Dump中畜普。這樣在分析問題時期丰,就更加方便了。
比如吃挑,如果我們將上述代碼第14行的park()改為:
LockSupport.park(this);
那么在線程Dump時钝荡,你可能會看到如下信息:
"t1" #8 prio=5 os_prio=0 tid=0x0117ac00 nid=0x2034 waiting on condition [0x15d0f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x048b4738> (a geym.conc.ch3.ls.LockSupport-
Demo$ChangeObjectThread)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at geym.conc.ch3.ls.LockSupportDemo$ChangeObjectThread.run
(LockSupportDemo.java:18)
- locked <0x048b2808> (a java.lang.Object)
注意,在堆棧中儒鹿,我們甚至還看到了當(dāng)前線程等待的對象化撕,這里就是ChangeObjectThread實例。
除了有定時阻塞的功能外约炎,LockSupport.park()還能支持中斷影響。但是和其他接收中斷的函數(shù)很不一樣蟹瘾,LockSupport.park()不會拋出InterruptedException異常圾浅。它只是會默默的返回,但是我們可以從Thread.interrupted()等方法獲得中斷標(biāo)記憾朴。
01 public class LockSupportIntDemo {
02 public static Object u = new Object();
03 static ChangeObjectThread t1 = new ChangeObjectThread("t1");
04 static ChangeObjectThread t2 = new ChangeObjectThread("t2");
05
06 public static class ChangeObjectThread extends Thread {
07 public ChangeObjectThread(String name){
08 super.setName(name);
09 }
10 @Override
11 public void run() {
12 synchronized (u) {
13 System.out.println("in "+getName());
14 LockSupport.park();
15 if(Thread.interrupted()){
16 System.out.println(getName()+" 被中斷了");
17 }
18 }
19 System.out.println(getName()+"執(zhí)行結(jié)束");
20 }
21 }
22
23 public static void main(String[] args) throws InterruptedException {
24 t1.start();
25 Thread.sleep(100);
26 t2.start();
27 t1.interrupt();
28 LockSupport.unpark(t2);
29 }
30 }
輸出
in t1
t1 被中斷了
t1 執(zhí)行結(jié)束
in t2
t2執(zhí)行結(jié)束
注意上述代碼在第27行狸捕,中斷了處于park()狀態(tài)的t1。之后众雷,t1可以馬上響應(yīng)這個中斷灸拍,并且返回。之后在外面等待的t2才可以進入臨界區(qū)砾省,并最終由LockSupport.unpark(t2)操作使其運行結(jié)束鸡岗。