java 利用同步工具類控制線程

 


 

 

前言

   參考來源:《java併發編程實戰》html

   同步工具類:根據工具類的自身狀態來協調線程的控制流。經過同步工具類,來協調線程之間的行爲。java

   可見性:在多線程環境下,當某個屬性被其餘線程修改後,其餘線程可以馬上看到修改後的信息。典型的是用java內置關鍵字volatile,volatile變量可讓jvm知道這個變量是對全部線程共享的,jvm會作必定的同步工做,但不保證原子性。編程

       這一部份內容能夠看一下http://www.cnblogs.com/dolphin0520/p/3920373.html安全

                  http://www.cnblogs.com/Mainz/p/3556430.html#多線程

   發佈和逸出:併發

        發佈:發佈一個對象指,使對象可以在當前做用域以外的代碼使用。舉個栗子就是,對象A持有對象C,把C的引用傳遞到其餘地方,或者外界訪問A有非私有方法返回C。發佈一個對象時要考慮是否有足夠理由發佈出去,不然可能會引發同步問題。jvm

           很明顯,同步工具類確定須要發佈給通訊雙方。ide

        逸出:當某個不該該發佈的對象被髮布時,叫作逸出。函數

  線程之間通訊,就須要把一個通訊協議(某個對象)發佈給雙方,而且對雙方來講都是可見的,這樣來協調線程之間的行爲。固然若是有特別須要,徹底能夠本身去寫同步工具類。工具

 


 

閉鎖

  閉鎖,可使線程等待某個事件(信號)發生後才執行後續操做。閉鎖有一個初始值,當初始值爲0的時候閉鎖終止,全部被該閉鎖阻塞的線程均可以繼續執行。假設有個線程A,咱們想A啓動以後須要等待線程B的信號才能繼續執行,這時候咱們能夠把一個閉鎖發佈給A,B,A等待閉鎖的結束,B來結束閉鎖,這樣A就能等B的命令才能繼續執行。事實上,利用閉鎖還可讓主線程等待全部子線程結束。有一點要注意的是,閉鎖是一次性的,一旦進入終止狀態(count==0),就不能被重置。

 

  CountDownLatch簡單用法介紹:

    public CountDownLatch(int count);  構造函數,當count爲0的時候,await方法再也不阻塞。

    public void countDown();         使count減1。

    public void await();          使當前線程阻塞,直到CountDownLatch的count爲0。

 

  下面這個例子顯示了利用閉鎖控制子線程執行和等待全部子線程結束。

 1 public class TestDownLatch {
 2     int count = 5;
 3     //控制子線程執行的閉鎖
 4     final CountDownLatch startGate = new CountDownLatch(1);
 5     //讓主線程等待全部子線程結束
 6     final CountDownLatch endGate = new CountDownLatch(count);
 7     
 8     //測試線程
 9     class Task extends Thread{
10         
11         int i;
12         
13         public Task(int i) {
14             this.i = i;
15         }
16 
17 
18         @Override
19         public void run() {
20             //等待閉鎖的結束
21             try {
22                 //等待主線程的命令
23                 startGate.await();
24                 System.out.println(i);
25                 //告訴主線程我已經作完了
26                 endGate.countDown();
27             } catch (InterruptedException igored) {
28             }
29         }
30         
31     }
32     
33     public void test(){
34         for (int i = 0; i < count; i++) {
35             Task task = new Task(i);
36             task.start();
37         }
38         System.out.println("全部線程已開啓");
39         try {
40             Thread.sleep(3000);
41             startGate.countDown();
42             System.out.println("全部子線程開始輸出");
43             
44             Thread.sleep(3000);
45             endGate.await();
46             System.out.println("主線程結束");
47         } catch (InterruptedException ignored) {
48         }
49         
50         
51     }
52     
53     public static void main(String[] args) {
54         TestDownLatch testDownLatch = new TestDownLatch();
55         testDownLatch.test();
56     }
57 
58 }
閉鎖

 運行結果:

 

 


 

FutureTask

  《java併發編程實戰》的翻譯者沒有翻譯這個詞,我也只能這麼叫它先。FutureTask自己實現Runnable接口和Future接口。

   FutureTask至關於一個助手,你把一個任務(有返回結果而且每每比較耗時)交給FutureTask,讓它先計算結果,而後你去幹別的事,這個助手(FutureTask)會幫你保存結果,等你須要結果時(調用Future#get),若是計算結束了,FutureTask直接把結果給你,若是還沒計算完,就讓你等待(阻塞),直到計算完了。

 

  FutureTask簡單用法介紹:

    public FutureTask(Callable<V> callable);  構造方法。參數Callable,至關於一種可生成結果的Runnable,至關於上面的任務(帶返回結果),V爲結果的類型,實現Callable須要實現call()。

    public V get();                獲取Callable返回的結果,若是Callable沒執行完,該方法會阻塞當前線程。

    public boolean cancel(boolean mayInterruptIfRunning);    取消執行

    另外還有isDone(),isCancelled()方法知道FutureTask狀態;

 

如下代碼測試FutureTask,先實例化一個FutureTask並自動獲取當前時間(一秒的消耗時間),輸出當時時間,而後三秒後再去FutureTask拿結果,能夠看到兩個時間沒有三秒間隔,說明FutureTask花費一秒後算好結果就保存起來,等主線程獲取結果。

 1 public class TestFutureTask {
 2     
 3     //任務,返回計算時的時間
 4     private final FutureTask<Date> future = new FutureTask<Date>(new Callable<Date>() {
 5         @Override
 6         public Date call() throws Exception {
 7             //1秒後再返回結果
 8             Thread.sleep(1000);
 9             return new Date();
10         }
11         
12     });
13     
14     public TestFutureTask(){
15         new Thread(future).start();
16     }
17     
18     public Date get() throws InterruptedException, ExecutionException{
19         return future.get();
20     }
21     
22     public static void main(String[] args) {
23         try {
24             System.out.println(new Date());
25             TestFutureTask task = new TestFutureTask();
26             System.out.println("等待3秒.............");
27             System.out.println(task.get());
28         } catch (InterruptedException e) {
29             e.printStackTrace();
30         } catch (ExecutionException e) {
31             e.printStackTrace();
32         }
33     }
34     
35 }
FutureTask

 

運行結果:

 

 


 

信號量

  計數信號量用來控制同時訪問某個特定資源的操做數量。信號量(Semaphore)維護的是一組許可,準確來講,信號量只是維護這組許可的數量。它有一個初始值,根據這個值的大小來決定每次請求許可時的行爲,若是用過鏈接池就很容易明白,鏈接池裏一開始放着一堆鏈接(Connection),每次從鏈接池拿出一個Connection,鏈接池就少一個Connection,每次Connection用完就還給鏈接池,方便之後的用戶使用。這些資源池都擁有一個信號量,來控制資源的出入。一樣,使用Semaphore能夠將任何一種容器變成有界阻塞容器。

  

  Semaphore簡單用法介紹:

    public Semaphore(int permits);  構造方法。參數premits爲初始化信號量大小,當permits爲0,獲取(調用acquire方法)資源會阻塞,直到permits>0。

    public void acquire() throws InterruptedException;   當permits>0時獲取許可,permits會減一,不然阻塞直到permits>0。

    public void release();                  釋放許可,permits加一。


如下代碼使用信號量簡單模擬鏈接池,測試讓鏈接池大小爲5,而後開六個線程去獲取鏈接,每一個線程持有鏈接3秒,能夠看到第六個線程阻塞住,要等有一個線程釋放鏈接才能夠獲取到鏈接

 1 public class Pool {
 2     private final List<Connection> list;
 3     private final Semaphore sem;
 4     
 5     public Pool(int initSize) {
 6         //注意這裏
 7         list = Collections.synchronizedList(new LinkedList<Connection>());
 8         for (int i = 0; i < initSize; i++) {
 9             list.add(new Connection());
10         }
11         sem = new Semaphore(initSize);
12     }
13     
14     //獲取鏈接
15     public Connection get() throws InterruptedException{
16         //若是sem當前許可數量爲0,阻塞當前線程
17         sem.acquire();
18         return list.remove(0);
19         
20     }
21     
22     //釋放鏈接
23     public void release(Connection resource) throws InterruptedException{
24         sem.release();
25         list.add(resource);
26         
27     }
28     
29     public static void main(String[] args) {
30         final Pool pool = new Pool(5);
31         
32         for (int i = 0; i < 6; i++) {
33             pool.new TestSemaphore(i, pool).start();
34         }
35     }
36     
37     public class TestSemaphore extends Thread{
38         
39         final int i;
40         final Pool pool;
41         
42         public TestSemaphore(int i,Pool pool) {
43             this.i = i;
44             this.pool = pool;
45         }
46 
47         @Override
48         public void run() {
49             try {
50                 Connection conn = pool.get();
51                 System.out.println(i+"成功獲取資源並佔用三秒!");
52                 Thread.sleep(3000);
53                 pool.release(conn);
54                 System.out.println(i+"已釋放資源!!!");
55             } catch (InterruptedException e) {
56                 System.out.println(i+"獲取資源失敗----");
57             }
58         }
59     }
60 
61 }
信號量

 

運行結果:

 

  特別注意:Pool的構造方法用Collections.synchronizedList方法使保存鏈接的list變成線程安全,在調用list的方法時會自動加鎖,可是若是用迭代器訪問list的時候須要手動加鎖,這一點在Collections的API文檔有特別說明。

 


 

柵欄

  柵欄跟閉鎖很像,先說柵欄的做用——阻塞一組線程直到某個事件發生。閉鎖和柵欄的最大區別就是,柵欄可讓全部線程必須同時到達柵欄位置,才能繼續執行,閉鎖最多作到線程執行到阻塞的位置必須等待閉鎖的終止,而沒法保證線程等待其餘線程的執行。柵欄就像賽馬的那個欄(我也不知道叫什麼),全部馬到了起點,柵欄一開,全部馬都開始跑,閉鎖是沒辦法知道全部馬都到達柵欄處的。

 

  CyclicBarrier簡單用法介紹:

    public CyclicBarrier(int parties, Runnable barrierAction);  構造方法。parties指一共有多少個線程,barrierAction就是全部線程經過柵欄的時回調函數。

    public int await() throws InterruptedException, BrokenBarrierException;  當前線程等待其餘全部線程到達(也是等待柵欄打開)。

    CyclicBarrier是指能夠循環使用的柵欄,每次柵欄打開後都能重置以便下次使用。可是若是對await的調用超時,或者await阻塞的線程被中斷,柵欄被認爲是打破,全部阻塞的await調用都拋出BrokenBarrierException。

 

 如下代碼用柵欄使全部五個召喚師都到達戰場後遊戲開始,從開始鏈接到全軍出擊的時間差應該是五個召喚師最慢那個,就是LOL時半天進度條也沒100的那個。雖然我戒擼好久了= =

 1 public class TestBarrier {
 2     private final CyclicBarrier barrier;
 3     
 4     public TestBarrier() {
 5         this.barrier = new CyclicBarrier(5, new Runnable() {
 6             @Override
 7             public void run() {
 8                 System.out.println(new Date()+"...全軍出擊!");
 9             }
10         });
11     }
12     
13     public void start(){
14         System.out.println(new Date()+"開始鏈接-----");
15         new Summoner(1,3).start();
16         new Summoner(2,1).start();
17         new Summoner(3,5).start();
18         new Summoner(4,6).start();
19         new Summoner(5,1).start();
20     }
21 
22     public class Summoner extends Thread{
23 
24         int i;
25         int preSecond;
26         
27         public Summoner(int i,int preSecond) {
28             this.i = i;
29             this.preSecond = preSecond;
30         }
31 
32         @Override
33         public void run() {
34             try {
35                 System.out.println("召喚師"+i+"須要"+preSecond+"秒到達戰場");
36                 Thread.sleep(preSecond*1000);
37                 barrier.await();
38                 System.out.println("------------ 召喚師"+i+":"+new Date());
39             } catch (InterruptedException | BrokenBarrierException e) {
40                 e.printStackTrace();
41             }
42         }
43         
44     }
45     
46     public static void main(String[] args) {
47         new TestBarrier().start();
48     }
49 
50 }
柵欄

 

 

運行結果:






 

小結

  閉鎖:當你的線程須要等待一個命令的時候能夠用它。

  FutureTask:當你有個耗時的過程想利用並行來提升效率,能夠用FutureTask。

  信號量:當你在多線程環境下想控制有限資源的訪問,使一個容器變成有界阻塞容器,能夠考慮信號量。

  柵欄:當你的多個線程須要共同完成某些步驟才能夠繼續執行,例如子問題結果合併,可使用柵欄。

相關文章
相關標籤/搜索