Java同步工具類

    上一篇文章我們介紹了一些Java平臺類庫的併發基礎構建模塊,介紹了一種Java同步工具類--阻塞隊列(鏈接點擊此處)。實際上還有一些其他的同步工具類,本文將介紹這些除阻塞隊列之外的同步工具類,信號量柵欄閉鎖


    一、閉鎖  CountDownLatch

    閉鎖是一種同步工具類,可以延遲線程的進度直到其達到終止狀態。閉鎖相當於門閂,閉鎖到達結束狀態之前,這扇門一直是關閉的,沒有任何線程能通過,當到達結束狀態時,這扇門會打開並且允許所有線程的通過。這是一次性的門閂,也就是打開後再也無法改變狀態再次關閉。這種東西可以保證某些活動直到其他活動都完成才繼續執行。

    可以使用在場景比如:

    1、保證必要資源的加載;

    2、確保某個服務只在其依賴的所有服務都啓動之後才啓動;

    3、某個操作的所有參與者都就緒了在啓動之後的操作。

    Java同步工具類中有閉鎖的實現,CountDownLatch的文檔描述如下:

    CountDownLatch,一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。這個閉鎖內存在一個計數表示需要完成的操作剩餘的需要操作的計數,當計數等於0,開始閉鎖下面的操作。

    常用方法如下:

  • await(); // 使當前線程在鎖存器倒計數至零之前一直等待
  • countDown(); // 遞減鎖存器的計數,如果計數到達零,則釋放所有等待的線程
  • getCount();//獲取閉鎖中的計數

    是不是覺得還是不是很理解,而且迷迷糊糊覺得和join()方法很像?不用擔心,下面我們通過一段代碼來解釋這個問題:

import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) {
        System.out.println("主線程開始...");
        CountDownLatch cdl = new CountDownLatch(1);
        MyThread myThread = new MyThread(cdl);
        myThread.start();
        try {
            cdl.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主線程繼續運行...");
        try {
            myThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主線程運行結束...");
    }
}

class MyThread extends Thread {
    private CountDownLatch countDownLatch;

    public MyThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            System.out.println("第一階段開始...");
            Thread.sleep(3000);
            System.out.println("第一階段結束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            countDownLatch.countDown();
        }
        System.out.println("第二階段開始...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第二階段結束...");

    }
}
    我們創建一個計數爲1的閉鎖cdl,傳入我們的myThread線程。在主線程啓動myThread線程,然後使用cdl.await()方法使得在閉鎖打開(在CountDownLatch中的計數爲0)之前下面的線程操作一直在等待。在MyThread第一階段結束後,調用門閂的countDown()方法,使得門閂中的計數減1,變成0,使得cdl.await()之後的操作可以繼續執行。

    我們在主線程中使用myThread.join()方法,使得主線程等待myThread直到其結束。

    運行結果如下:


    實際上這個例子就體現出了門閂和join()的區別:

    對於閉鎖對象cdl,是在某個其他線程的內部操作的結束之後調用cdl.countDown()方法,這個方法最好寫在finally塊內,保證countDown()方法的執行,否則countDown()執行出現問題會導致死鎖。cdl.await()方法則是在此處設立閉鎖閉鎖的計數不等於0之前,該閉鎖之後的操作不能執行。

    而myThread.join()則是在myThread線程完全執行完之前這行代碼之後的操作不能執行。

    也就是閉鎖是可以在線程沒有完全執行完就打開閉鎖繼續未執行代碼的(比如例子中在第一階段結束就打開閉鎖),而join()方法一定要線程執行完畢才能繼續未執行的代碼(比如例子中第二階段結束才繼續)。


二、FutureTask

    FutureTask也可以被用作閉鎖(FutureTask實現了Future接口,實際上使用了多線程設計模式中的Future Pattern,之前寫過,鏈接點擊此處)。

    Future.get()的欣慰取決於任務的狀態。任務完成該方法會立即返回結果,否則該方法將進入阻塞狀態,直到任務完成,然後返回結果或者拋出異常。FutureTask將計算結果從執行計算的線程傳遞到獲取這個結果的線程,而Future的規範確保了這種傳遞過程能實現結果的安全發佈。

    Future是一個接口,下面約定了一個V call() throws Exception方法,Runnable接口則是約定了void run()方法。FutureTask實現了這兩個接口,其中Callable的實現需要傳入,而run()方法在FutureTask內部實現。我們傳入Callable的實現,就可以使用了。通過FutureTask的get()方法就可以阻塞獲得call()方法返回的計算結果。

    FutureTask在Executor框架中表示異步任務,我們可以利用這個特點把FutureTask當作閉鎖使用。舉個例子:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis(); 
        System.out.println("主線程開始...");
        FutureTask<Integer> future = new FutureTask<Integer>(new Task());
        System.out.println("進行Task任務計算的子線程開始...");
        new Thread(future).start();;
        try {
            System.out.println("主線程正在執行自己的任務...");
            Thread.sleep(1000);
            System.out.println("主線程嘗試獲取Task結果...");
            System.out.println("時間過去"+(System.currentTimeMillis()-startTime));
            System.out.println("主線程獲取到結果爲:"+future.get());
            System.out.println("時間過去"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("主線程結束");
    }
}
class Task implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        //花3s模擬計算過程
        Thread.sleep(3000);
        //模擬計算結果是1
        return 1;
    }
}
    運行結果如下:


    我們來分析一下代碼和運行結果。我們在主線程創建FutureTask實例future,傳入Callable接口。啓動匿名線程new Thread(future)。然後模擬主線程花費1s的時間執行其他的任務,之後使用future.get()阻塞獲取Task的任務執行結果。從嘗試future.get()開始,2s之後獲取到執行結果,最後主線程結束。

    我們可以看到實際上我們可以使用FutureTask的實例future的get()方法作爲一個門閂,在callable接口的call()方法執行完之前future.get()會一直阻塞,下面的代碼不會執行。直到該方法獲取到了結果,下面代碼纔會執行,因此也可以當作一個閉鎖


三、信號量  Semaphore

    信號量可以用來控制多個線程訪問某個有限資源的資源池。在同步工具類的體現爲Semaphore,其常用方法爲acquire()獲取一個許可,使得信號量減少一個許可,release()釋放一個許可,而Semaphore中維護着許可的個數。acquire()方法和release()方法是阻塞的,也就是獲取不到這個許可或者釋放不了這個序列會一直阻塞,直到可以獲取/釋放,而且Semaphore還提供了tryAcquire()方法和tryRelease()方法,這是無阻塞,獲取/釋放不了許可則直接返回false。

    爲了保證release()方法一定會執行,最好寫在finally塊中。

    我們使用信號量來模擬實現30個線程從10個數據庫連接池中獲取一個連接,最後30個線程都存儲完畢的過程:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

//模擬數據庫存儲過程的線程 只有CONNECTION_NUM個數據庫 但是有THREAD_NUM個線程
//使用信號量Semaphore來解決這個公用資源有限的問題
class SaveRunnableImpl implements Runnable {
    //線程共享的信號量
    private final Semaphore sem;
    public SaveRunnableImpl(Semaphore sem){
        this.sem = sem;
    }
    @Override
    public void run() {
        try {
            //請求資源 資源個數減一
            sem.acquire();
            try{
                System.out.println(Thread.currentThread()+"存儲了數據");
                Thread.sleep(3000);
            }
            finally{
                //釋放資源 資源個數加一 最好把release寫到finally塊中 保證release()一定會執行
                sem.release();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    private static int THREAD_NUM = 30;
    private static int CONNECTION_NUM = 10;
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_NUM);
        Semaphore sem = new Semaphore(CONNECTION_NUM);
        for(int i = 0; i < THREAD_NUM; i++){
            threadPool.execute(new SaveRunnableImpl(sem));
        }
        threadPool.shutdown();
    }
}
    運行結果應該是10個一批10個一批的線程執行了存儲操作。


四、柵欄  CyclicBarrier/Exchanger

    前面我們介紹了閉鎖,用來啓動一組相關的操作,直到這些相關操作做完纔可以打開閉鎖,使後面的操作能夠繼續。閉鎖是一次性的,一旦進入終止狀態就不能再重置。

    柵欄和閉鎖比較類似,柵欄能使得一組線程在某個事件全都發生之前使得線程互相等待。這就保證了這一組線程同時到達柵欄處才繼續執行。

    柵欄在Java同步工具類的其中一個體現是CyclicBarrier,構造方法有兩個:

   

//創建一個新的柵欄,當給定數量的線程都在等待它時,柵欄打開,但是不會執行預定於的動作
CyclicBarrier(int parties);
   
//創建一個新的柵欄,在給定數量的線程等待時,柵欄打開,最後一個進入柵欄的線程執行預定義操作
CyclicBarrier(int parties, Runnable barrierAction);
    常用方法如下:

   

//等待直到parties個數的線程都在等待
int await();

//等待直到parties個數的線程都在等待或者指定過去過去
int await(long timeout, TimeUnit unit);

//將屏障重置爲初始化狀態(柵欄可重用 和閉鎖不同)
void reset();
    如果await()調用超時或者阻塞的線程被中斷,那麼柵欄就被打破了,所有阻塞的await()調用的地方都會終止並且拋出BrokenBarrierException

    我們下面使用屏障來模擬這麼一種場景,三個不同的線程執行各自的任務,執行完後在柵欄處等待,等待結束輸出自己的等待時間,再給柵欄進行事件預定義,執行該定義事件:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

class Thread1 extends Thread{
    private final CyclicBarrier barrier;
    public Thread1 (CyclicBarrier barrier){
        this.barrier = barrier;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            //這1s模擬運行Thread1處理時間
            Thread.sleep(1000);
            //只要等待的線程個數沒有達到要求 就一直互相等待
            barrier.await();
            System.out.println(Thread.currentThread()+"等待結束,等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

class Thread2 extends Thread{
    private final CyclicBarrier barrier;
    public Thread2 (CyclicBarrier barrier){
        this.barrier = barrier;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            //這2s模擬運行Thread2處理時間
            Thread.sleep(2000);
            //只要等待的線程個數沒有達到要求 就一直互相等待
            barrier.await();
            System.out.println(Thread.currentThread()+"等待結束,等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

class Thread3 extends Thread{
    private final CyclicBarrier barrier;
    public Thread3 (CyclicBarrier barrier){
        this.barrier = barrier;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            //這3s模擬運行Thread3處理時間
            Thread.sleep(3000);
            //只要等待的線程個數沒有達到要求 就一直互相等待
            barrier.await();
            System.out.println(Thread.currentThread()+"等待結束,等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        //創建柵欄
        CyclicBarrier barrier = new CyclicBarrier(3,new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread()+"全部線程到達");
            }
        });
        new Thread1(barrier).start();
        new Thread2(barrier).start();
        new Thread3(barrier).start();
    }
}
   

    Thread1處理自己的任務花了1s,Thread2處理自己的任務花了2s,Thread3處理自己的任務花了3s,執行完自己的任務每個線程就都在柵欄處等待,輸出結果如下:


    也就是雖然各個線程處理自己任務的時間不一樣,但是最終等待的時間實際上是取決於最後到達線程的等待時間


    柵欄在Java同步工具包中的體現還有一個Exchanger,是一個雙方柵欄,每一個在柵欄處交換數據。當雙方執行的操作不對稱的時候,Exchanger會很有用。當雙方線程都到達柵欄的時候,將雙方的數據進行交換,這個Exchanger對象可以使得兩個線程生成的對象能夠安全地交換。

    這個類只提供了一個空構造函數,提供了兩個方法:

    exchange(V x);//交換雙方線程生成對象 交換成功或者被中斷

    exchange(V x,long timeout, TimeUnit unit);//交換雙方線程生成對象 交換成功或者超時拋出超時異常或者被中斷

    我們使用這個柵欄類模擬以下場景,兩個線程,一個線程沉睡3000ms後交換字符串,一個線程直接交換字符串,互相輸出接收到的字符串已經等待時間:

import java.util.concurrent.Exchanger;

class Thread1 extends Thread{
    private Exchanger<String> exchanger;
    private String name;
    public Thread1(String name,Exchanger<String> exchanger){
        super(name);
        this.exchanger = exchanger;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            Thread.sleep(3000);
            System.out.println(Thread.currentThread()+"獲取到數據:"+exchanger.exchange("我是Thread1的實例"));
            System.out.println("等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class Thread2 extends Thread{
    private Exchanger<String> exchanger;
    private String name;
    public Thread2(String name,Exchanger<String> exchanger){
        super(name);
        this.exchanger = exchanger;
    }
    @Override
    public void run(){
        try {
            long startTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread()+"獲取到數據:"+exchanger.exchange("我是Thread2的實例"));
            System.out.println("等待了"+(System.currentTimeMillis()-startTime));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<String>();
        new Thread1("thread1",exchanger).start();
        new Thread2("thread2",exchanger).start();
    }
}
    運行結果如下:


    也就是交換雙方先到柵欄處的會等待後到達柵欄處的,直到交換雙方都到達柵欄然後開始交換數據。


    以上就是常用的Java同步工具類--阻塞隊列(見文章開頭鏈接)、柵欄、信號量、FutureTask、閉鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章