Java併發編程系列-(2) 線程的併發工具類

2.線程的併發工具類

2.1 Fork-Join

JDK 7中引入了fork-join框架,專門來解決計算密集型的任務。可以將一個大任務,拆分成若干個小任務,如下圖所示:

Picture1.png

Fork-Join框架利用了分而治之的思想:什麼是分而治之?規模爲N的問題,N<閾值,直接解決,N>閾值,將N分解爲K個小規模子問題,子問題互相對立,與原問題形式相同,將子問題的解合併得到原問題的解.

具體使用中,需要向ForkJoinPool線程池提交一個ForkJoinTask任務。ForkJoinTask任務有兩個重要的子類,RecursiveAction類和RecursiveTask類,他們分別表示沒有返回值的任務和可以有返回值的任務。

RecursiveAction類

下面的例子中,我們使用RecursiveAction遍歷指定目錄來尋找特定類型文件,需要實現compute方法。

public class FindDirsFiles extends RecursiveAction{

    private File path;//當前任務需要搜尋的目錄

    public FindDirsFiles(File path) {
        this.path = path;
    }

    public static void main(String [] args){
        try {
            // 用一個 ForkJoinPool 實例調度總任務
            ForkJoinPool pool = new ForkJoinPool();
            FindDirsFiles task = new FindDirsFiles(new File("F:/"));

            pool.execute(task);//異步調用

            System.out.println("Task is Running......");
            Thread.sleep(1);
            int otherWork = 0;
            for(int i=0;i<100;i++){
                otherWork = otherWork+i;
            }
            System.out.println("Main Thread done sth......,otherWork="+otherWork);
            task.join();//阻塞的方法
            System.out.println("Task end");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void compute() {
        
        List<FindDirsFiles> subTasks = new ArrayList<>();
        
        File[] files = path.listFiles();
        if(files!=null) {
            for(File file:files) {
                if(file.isDirectory()) {
                    subTasks.add(new FindDirsFiles(file));
                }else {
                    //遇到文件,檢查
                    if(file.getAbsolutePath().endsWith("txt")) {
                        System.out.println("文件:"+file.getAbsolutePath());
                    }
                }
            }
            if(!subTasks.isEmpty()) {
                for(FindDirsFiles subTask:invokeAll(subTasks)) {
                    subTask.join();//等待子任務執行完成
                }
            }
        }
    }
}

RecursiveTask類

下面的例子中,利用RecursiveTask來實現數值累加。

public class SumArray {
    private static class SumTask extends RecursiveTask<Integer>{

        private final static int THRESHOLD = MakeArray.ARRAY_LENGTH/10;
        private int[] src; //表示我們要實際統計的數組
        private int fromIndex;//開始統計的下標
        private int toIndex;//統計到哪裏結束的下標

        public SumTask(int[] src, int fromIndex, int toIndex) {
            this.src = src;
            this.fromIndex = fromIndex;
            this.toIndex = toIndex;
        }

        @Override
        protected Integer compute() {
            if(toIndex-fromIndex < THRESHOLD) {
                int count = 0;
                for(int i=fromIndex;i<=toIndex;i++) {
                    //SleepTools.ms(1);
                    count = count + src[i];
                }
                return count;
            }else {
                //fromIndex....mid....toIndex
                //1...................70....100
                int mid = (fromIndex+toIndex)/2;
                SumTask left = new SumTask(src,fromIndex,mid);
                SumTask right = new SumTask(src,mid+1,toIndex);
                invokeAll(left,right);
//              left.fork();
//              right.fork();
                return left.join()+right.join();
            }
        }
    }


    public static void main(String[] args) {

        ForkJoinPool pool = new ForkJoinPool();
        int[] src = MakeArray.makeArray();

        SumTask innerFind = new SumTask(src,0,src.length-1);

        long start = System.currentTimeMillis();

        pool.invoke(innerFind);//同步調用
        System.out.println("Task is Running.....");

        System.out.println("The count is "+innerFind.join()
                +" spend time:"+(System.currentTimeMillis()-start)+"ms");

    }
}

注意到fork和invokeAll都能達到相同的效果,只是fork將task交給工作線程後立刻返回;但是invokeAll會fork其中一個任務後,同時同步的調用另外一個任務,然後等待兩個任務完成,可以參考invokeAll的實現:

ublic static void invokeAll(ForkJoinTask<?> t1, ForkJoinTask<?> t2) {
        int s1, s2;
        t2.fork();
        if (((s1 = t1.doInvoke()) & ABNORMAL) != 0)
            t1.reportException(s1);
        if (((s2 = t2.doJoin()) & ABNORMAL) != 0)
            t2.reportException(s2);
    }

工作密取(Work Stealing)

在後臺,fork-join框架使用了一種有效的方法來平衡可用線程的負載,稱爲工作密取(Work stealing)。每個工作線程都有一個雙端隊列(deque)來完成任務,一個工作線程將子任務壓入其雙端隊列的隊頭。當一個工作線程空閒時,它會從另一個雙端隊列的隊尾密取一個任務。

ForkJoinPool內部利用循環數組實現了一個雙端隊列,稱爲WorkQueue。對於這個Queue,有3種操作方法,分別是push、pop和poll。對於push和pop操作,只能被擁有該Queue的線程所調用。poll操作被用於其他工作線程從該Queue中獲得task。

考慮到多線程steal work的情況,當進行poll操作時,會通過CAS操作來保證多線程下的安全性。如果CAS操作成功,則說明竊取成功。

Screen Shot 2019-11-29 at 10.20.11 PM.png

更多細節可以查看ForkJoinPool的實現以及論文https://www.dre.vanderbilt.edu/~schmidt/PDF/work-stealing-dequeue.pdf。

invoke && execute && submit 區別

invoke是同步調用,它會馬上執行執行,並且將task join到當前線程,也就是阻塞當前線程。

    /**
     * Performs the given task, returning its result upon completion.
     * If the computation encounters an unchecked Exception or Error,
     * it is rethrown as the outcome of this invocation.  Rethrown
     * exceptions behave in the same way as regular exceptions, but,
     * when possible, contain stack traces (as displayed for example
     * using {@code ex.printStackTrace()}) of both the current thread
     * as well as the thread actually encountering the exception;
     * minimally only the latter.
     *
     * @param task the task
     * @param <T> the type of the task's result
     * @return the task's result
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public <T> T invoke(ForkJoinTask<T> task) {
        if (task == null)
            throw new NullPointerException();
        externalSubmit(task);
        return task.join();
    }

execute和submit是異步調用,它會將Task送到Work Queue中等待運行。如果需要看到運行結果,可以在execute和submit後調用join方法。兩者的區別只是submit會返回task,execute返回空值。

    /**
     * Submits a ForkJoinTask for execution.
     *
     * @param task the task to submit
     * @param <T> the type of the task's result
     * @return the task
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
        return externalSubmit(task);
    }

下面是execute的實現:

    /**
     * Arranges for (asynchronous) execution of the given task.
     *
     * @param task the task
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public void execute(ForkJoinTask<?> task) {
        externalSubmit(task);
    }

2.2 CountDownLatch

Latch是門栓的意思,顧名思義,CountDownLatch是一個多線程的控制工具類。通常用於讓一組線程等待直到倒計時結束,再開始執行。

Screen Shot 2019-11-29 at 11.22.20 PM.png

CountDownLatch的用法如下,

  1. 初始化count down的次數;
  2. 在初始化線程中調用countDown對計數器進行減1;
  3. 工作線程中調用await進行等待,當計時器爲0時,工作線程開始工作。
public class UseCountDownLatch {
    
    static CountDownLatch latch = new CountDownLatch(6);

    // 初始化線程(只有一步,有4個)
    private static class InitThread implements Runnable{

        @Override
        public void run() {
            System.out.println("Thread_"+Thread.currentThread().getId() +" finish init work......");
            
            latch.countDown();//初始化線程完成工作了,countDown方法只扣減一次;
            
            // We can add some tasks after the countDown is invoked
            for(int i =0;i<2;i++) {
                System.out.println("Thread_"+Thread.currentThread().getId() +" ........continue do its work");
            }
        }
    }
    
    // 業務線程
    private static class BusinessThread implements Runnable{
        @Override
        public void run() {
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            for(int i =0;i<3;i++) {
                System.out.println("BusinessThread_"+Thread.currentThread().getId() +" start to do business-----");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 單獨的初始化線程,初始化分爲2步,需要扣減兩次
        new Thread(new Runnable() {
            @Override
            public void run() {
                SleepTools.ms(1);
                System.out.println("Thread_"+Thread.currentThread().getId() +" finish init work step 1st......");
                latch.countDown();//每完成一步初始化工作,扣減一次
                
                System.out.println("begin step 2nd.......");
                SleepTools.ms(1);
                System.out.println("Thread_"+Thread.currentThread().getId() +" finish init work step 2nd......");
                latch.countDown();//每完成一步初始化工作,扣減一次
            }
        }).start();
        
        new Thread(new BusinessThread()).start();
        
        // Start 3 new init thread
        for(int i=0;i<=3;i++){
            Thread thread = new Thread(new InitThread());
            thread.start();
        }

        latch.await();
        
        System.out.println("Main do ites work........");
    }
}

2.3 CyclicBarrier

CyclicBarrier類實現了一個集結點,稱爲屏障(barrier)。當一個線程完成了那部分任務之後,它運行到屏障處,一旦所有線程都到達了這個屏障,屏障就撤銷,線程就可以繼續運行了。

Screen Shot 2019-11-30 at 1.03.28 PM.png

CyclicBarrier的用法如下:

  1. 構造一個Barrier,需要給出參與的線程數。JDK裏提供了兩個構造函數,barrierAction爲屏障打開之後需要執行的action。
CyclicBarrier(int parties) 

CyclicBarrier(int parties, Runnable barrierAction)
  1. 每個線程做一些事情,完成後在屏障上調用await等待,
public void run() {
    doSomeWork();
    barrier.await();
    ...
}
  1. 當所有線程都到達了await後,此時屏障打開。如果有定義屏障打開後執行的action,則會先執行action。然後其他線程繼續往下執行await後面的部分。

下面是具體的例子:

在打開屏障後,輸出了各個線程的id。

public class UseCyclicBarrier {
    
    private static CyclicBarrier barrier = new CyclicBarrier(5,new TaskAfterBarrierIsOpenThread());
    
    private static ConcurrentHashMap<String,Long> resultMap = new ConcurrentHashMap<>();//存放子線程工作結果的容器

    public static void main(String[] args) {
        for(int i=0;i< 5;i++){
            Thread thread = new Thread(new SubThread());
            thread.start();
        }
    }

    //負責屏障開放以後的工作
    private static class TaskAfterBarrierIsOpenThread implements Runnable{

        @Override
        public void run() {
            StringBuilder result = new StringBuilder();
            for(Map.Entry<String,Long> workResult:resultMap.entrySet()){
                result.append("["+workResult.getValue()+"]");
            }
            System.out.println(" the result = "+ result);
            System.out.println("do other business........");
        }
    }

    //工作線程
    private static class SubThread implements Runnable{

        @Override
        public void run() {
            long id = Thread.currentThread().getId();//線程本身的處理結果
            resultMap.put(Thread.currentThread().getId()+"",id);
            Random r = new Random();//隨機決定工作線程的是否睡眠
            try {
                if(r.nextBoolean()) {
                    Thread.sleep(2000+id);
                    System.out.println("Thread_"+id+" ....do something ");
                }
                System.out.println(id+"....is await");
                barrier.await();
                Thread.sleep(1000+id);
                System.out.println("Thread_"+id+" ....do its business ");
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

注意屏障是可以重複使用的,當所有等待線程被釋放後可以被重用。

CountDownLatch和CyclicBarrier對比
countdownlatch的放行由第三者控制,CyclicBarrier放行由一組線程本身控制
countdownlatch放行條件 >= 線程數,CyclicBarrier放行條件 = 線程數

2.4 Semaphore

Semaphore也叫信號量,在JDK1.5被引入,可以用來控制同時訪問特定資源的線程數量,通過協調各個線程,以保證合理的使用資源。

Semaphore內部維護了一組虛擬的許可,許可的數量可以通過構造函數的參數指定。

  • 訪問特定資源前,必須使用acquire方法獲得許可,如果許可數量爲0,該線程則一直阻塞,直到有可用許可。
  • 訪問資源後,使用release釋放許可。

示例程序如下:

public class MySemaphoreTest {
    static Semaphore semaphore = new Semaphore(4);
    
    private static class BusinessThread extends Thread {
        String name = "";

        BusinessThread(String name) {
            this.name = name;
        }
        
        @Override
        public void run() {
            try {
                System.out.println(name + " try to acquire lock...");
                System.out.println(name + " : available Semaphore permits now: " + semaphore.availablePermits());
                
                semaphore.acquire();
                
                System.out.println(name + " : got the permit!");            
                
                // Do some business work
                Thread.sleep(1000);
                
                System.out.println(name + " : release lock...");
                semaphore.release();
                
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 6; i++) {
            new BusinessThread((i+1) + "").start();
        }
    }
}

2.5 Exchanger

當兩個線程在同一個數據緩衝區的兩個實例上工作時,就可以使用Exchanger。典型的情況是,一個線程向緩衝區填入數據,另一個線程消耗這些數據。當他們都完成之後,相互交換緩衝區。

下面的例子中,在兩個線程中分別填入數據,然後交換數據,最後打印從對方線程交換得來的數據。

public class UseExchange {
    private static final Exchanger<Set<String>> exchange 
        = new Exchanger<Set<String>>();

    public static void main(String[] args) {

        //第一個線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                Set<String> setA = new HashSet<String>();//存放數據的容器
                try {
                    setA.add("1");
                    System.out.println(Thread.currentThread().getName()  + " Add 1 to the set");                    
                    setA.add("2");
                    System.out.println(Thread.currentThread().getName()  + " Add 2 to the set");
                    setA.add("3");
                    System.out.println(Thread.currentThread().getName()  + " Add 3 to the set");
                       
                    setA = exchange.exchange(setA);//交換setA出去,返回交換來的數據setB
                    
                    /*處理交換後的數據*/
                    System.out.println(Thread.currentThread().getName()  + " print the data after exchange ");
                    setA.forEach(string -> System.out.println(Thread.currentThread().getName() + " print" + string));
                } catch (InterruptedException e) {
                }
            }
        }).start();

      //第二個線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                Set<String> setB = new HashSet<String>();//存放數據的容器
                try {
                    setB.add("A");
                    System.out.println(Thread.currentThread().getName()  + " Add A to the set");
                    
                    setB.add("B");
                    System.out.println(Thread.currentThread().getName()  + " Add B to the set");
                    
                    setB.add("C");
                    System.out.println(Thread.currentThread().getName()  + " Add C to the set");
                    
                    setB = exchange.exchange(setB);//交換setB出去,返回交換來的數據setA
                    
                    /*處理交換後的數據*/
                    System.out.println(Thread.currentThread().getName()  + " print the data after exchange ");
                    setB.forEach(string -> System.out.println(Thread.currentThread().getName() + " print" + string));
                } catch (InterruptedException e) {
                }
            }
        }).start();
    }
}

2.6 Future、Callable和FutureTask

Future是多線程開發中的一種常見設計模式,核心思想是異步調用。當需要調用一個函數方法時,如果這個函數很慢,需要進行等待,這時可以先處理一些其他任務,在真正需要數據的時候再去嘗試獲得需要的數據。

Picture1.png

以上是Future的基本結構,RunnableFuture繼承了Future和Runnable接口,FutureTask可以接收一個Callable實例作爲運行的任務。

Future的使用比較簡單,例子如下:

public class UseFuture {
    
    /*實現Callable接口,允許有返回值*/
    private static class UseCallable implements Callable<Integer>{

        private int sum;
        @Override
        public Integer call() throws Exception {
            System.out.println("Callable子線程開始計算");
            Thread.sleep(2000);
            for(int i=0;i<5000;i++) {
                sum = sum+i;
            }
            System.out.println("Callable子線程計算完成,結果="+sum);
            return sum;
        }

    }
    
    public static void main(String[] args) 
            throws InterruptedException, ExecutionException {
        
        UseCallable useCallable = new UseCallable();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(useCallable);
        new Thread(futureTask).start();
        
        Random r = new Random();
        SleepTools.second(1);
        if(r.nextBoolean()) {//隨機決定是獲得結果還是終止任務
            System.out.println("Get UseCallable result = "+futureTask.get());
        }else {
            System.out.println("中斷計算");
            futureTask.cancel(true);
        }
    }
}

注意Future接口中聲明瞭5個方法,分別爲:

  • cancel方法:用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。參數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設置true,則表示可以取消正在執行過程中的任務。如果任務已經完成,則無論mayInterruptIfRunning爲true還是false,此方法肯定返回false,即如果取消已經完成的任務會返回false;如果任務正在執行,若mayInterruptIfRunning設置爲true,則返回true,若mayInterruptIfRunning設置爲false,則返回false;如果任務還沒有執行,則無論mayInterruptIfRunning爲true還是false,肯定返回true。
  • isCancelled方法:表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。
  • isDone方法:表示任務是否已經完成,若任務完成,則返回true;
  • get()方法:用來獲取執行結果,這個方法會產生阻塞,會一直等到任務執行完畢才返回;
  • get(long timeout, TimeUnit unit):用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。

本文由『後端精進之路』原創,首發於博客 http://teckee.github.io/ , 轉載請註明出處

搜索『後端精進之路』關注公衆號,立刻獲取最新文章和價值2000元的BATJ精品面試課程

後端精進之路.png

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