《實戰(zhàn)高并發(fā)程序設(shè)計》讀書筆記-多線程團隊協(xié)作:同步控制

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é)束鸡岗。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市编兄,隨后出現(xiàn)的幾起案子轩性,更是在濱河造成了極大的恐慌,老刑警劉巖狠鸳,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揣苏,死亡現(xiàn)場離奇詭異,居然都是意外死亡件舵,警方通過查閱死者的電腦和手機卸察,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铅祸,“玉大人坑质,你說我怎么就攤上這事。” “怎么了洪乍?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵眯杏,是天一觀的道長。 經(jīng)常有香客問我壳澳,道長岂贩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任巷波,我火速辦了婚禮萎津,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抹镊。我一直安慰自己锉屈,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布垮耳。 她就那樣靜靜地躺著颈渊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪终佛。 梳的紋絲不亂的頭發(fā)上俊嗽,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音铃彰,去河邊找鬼绍豁。 笑死,一個胖子當(dāng)著我的面吹牛牙捉,可吹牛的內(nèi)容都是我干的竹揍。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼邪铲,長吁一口氣:“原來是場噩夢啊……” “哼芬位!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起霜浴,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤晶衷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后阴孟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晌纫,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年永丝,在試婚紗的時候發(fā)現(xiàn)自己被綠了锹漱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡慕嚷,死狀恐怖哥牍,靈堂內(nèi)的尸體忽然破棺而出毕泌,到底是詐尸還是另有隱情,我是刑警寧澤嗅辣,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布撼泛,位于F島的核電站,受9級特大地震影響澡谭,放射性物質(zhì)發(fā)生泄漏愿题。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一蛙奖、第九天 我趴在偏房一處隱蔽的房頂上張望潘酗。 院中可真熱鬧,春花似錦雁仲、人聲如沸仔夺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缸兔。三九已至,卻和暖如春祭衩,著一層夾襖步出監(jiān)牢的瞬間灶体,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工掐暮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人政钟。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓路克,卻偏偏與公主長得像,于是被迫代替她去往敵國和親养交。 傳聞我的和親對象是個殘疾皇子精算,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容