百度的一道 java 高頻面試題的多種解法

考試結束,班級平均分只拿到了年級第二,班主任因而問道:你們都知道世界第一高峯珠穆朗瑪峯,有人知道世界第二高峯是什麼嗎?正當班主任要繼續發話,只聽到角落默默想起來一個聲音:」喬戈裏峯html

前言

文章出自:www.cnblogs.com/dudu19939/p… 這道題是羣裏的朋友的投稿,上面是這位朋友的原文博客連接,這道題目我在百度的面試也遇到過,當時這位朋友在這個問題的博客也與我交流過,我也貢獻了其中一種方法,嘿嘿,最後看了這位朋友的成文,以爲寫得很不錯,望與諸君共勉(PS:歡迎你們投稿)。java

題目

下面是我在2018年10月11日二面百度的時候的一個問題:程序員

java程序,主進程須要等待多個子進程結束以後再執行後續的代碼,有哪些方案能夠實現? 這個需求其實咱們在工做中常常會用到,好比用戶下單一個產品,後臺會作一系列的處理,爲了提升效率,每一個處理均可以用一個線程來執行,全部處理完成了以後纔會返回給用戶下單成功,歡迎你們批評指正。面試

解法

1.join方法

使用Thread的join()等待全部的子線程執行完畢,主線程在執行,thread.join()把指定的線程加入到當前線程,能夠將兩個交替執行的線程合併爲順序執行的線程。好比在線程B中調用了線程A的join()方法,直到線程A執行完畢後,纔會繼續執行線程B。bash

import java.util.Vector;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Vector<Thread> vector = new Vector<>();
        for(int i=0;i<5;i++) {
            Thread childThread= new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println("子線程被執行");
                }
                
            });
            vector.add(childThread);
            childThread.start();
        }
        for(Thread thread : vector) {
            thread.join();
        }
        System.out.println("主線程被執行");
    }
複製代碼

執行結果

子線程被執行
子線程被執行
子線程被執行
子線程被執行
子線程被執行
主線程被執行
複製代碼

2.等待多線程完成的CountDownLatch

CountDownLatch的概念微信

CountDownLatch是一個同步工具類,用來協調多個線程之間的同步,或者提及到線程之間的通訊(而不是用做互斥的做用)。多線程

CountDownLatch可以使一個線程在等待另一些線程完成各自工做以後,再繼續執行。使用一個計數器進行實現。計數器初始值爲線程的數量。當每個線程完成本身任務後,計數器的值就會減一。當計數器的值爲0時,表示全部的線程都已經完成了任務,而後在CountDownLatch上等待的線程就能夠恢復執行任務。 CountDownLatch的用法併發

CountDownLatch典型用法1:某一線程在開始運行前等待n個線程執行完畢。將CountDownLatch的計數器初始化爲n new CountDownLatch(n) ,每當一個任務線程執行完畢,就將計數器減1 countdownlatch.countDown(),當計數器的值變爲0時,在CountDownLatch上 await() 的線程就會被喚醒。一個典型應用場景就是啓動一個服務時,主線程須要等待多個組件加載完畢,以後再繼續執行。ide

CountDownLatch典型用法2:實現多個線程開始執行任務的最大並行性。注意是並行性,不是併發,強調的是多個線程在某一時刻同時開始執行。相似於賽跑,將多個線程放到起點,等待發令槍響,而後同時開跑。作法是初始化一個共享的CountDownLatch(1),將其計數器初始化爲1,多個線程在開始執行任務前首先 coundownlatch.await(),當主線程調用 countDown() 時,計數器變爲0,多個線程同時被喚醒。 CountDownLatch的不足工具

CountDownLatch是一次性的,計數器的值只能在構造方法中初始化一次,以後沒有任何機制再次對其設置值,當CountDownLatch使用完畢後,它不能再次被使用。

import java.util.Vector;
import java.util.concurrent.CountDownLatch;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(5);
        for(int i=0;i<5;i++) {
            Thread childThread= new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println("子線程被執行");
                    latch.countDown();
                }
                
            });
            
            childThread.start();
            
        }
        latch.await();//阻塞當前線程直到latch中的值
        System.out.println("主線程被執行");
    }
    
}
複製代碼

執行結果:

子線程被執行
子線程被執行
子線程被執行
子線程被執行
子線程被執行
主線程被執行
複製代碼

3.同步屏障CyclicBarrier

這裏必須注意,CylicBarrier是控制一組線程的同步,初始化的參數:5的含義是包括主線程在內有5個線程,因此只能有四個子線程,這與CountDownLatch是不同的。

countDownLatch和cyclicBarrier有什麼區別呢,他們的區別:countDownLatch只能使用一次,而CyclicBarrier方法可使用reset()方法重置,因此CyclicBarrier方法能夠能處理更爲複雜的業務場景。

我曾經在網上看到一個關於countDownLatch和cyclicBarrier的形象比喻,就是在百米賽跑的比賽中若使用 countDownLatch的話衝過終點線一我的就給評委發送一我的的成績,10我的比賽發送10次,若是用CyclicBarrier,則只在最後一我的衝過終點線的時候發送全部人的數據,僅僅發送一次,這就是區別。

package interview;

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

public class Test3 {
    public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
        final CyclicBarrier barrier = new CyclicBarrier(5);
        for(int i=0;i<4;i++) {
            Thread childThread= new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println("子線程被執行");
                    try {
                        barrier.await();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                
            });
            
            childThread.start();
            
        }
        barrier.await();//阻塞當前線程直到latch中的值
        System.out.println("主線程被執行");
    }
}
複製代碼

執行結果:

子線程被執行
子線程被執行
子線程被執行
子線程被執行
主線程被執行
複製代碼

4.使用yield方法(注意此種方法通過親自試驗證實並不可靠!)

public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<5;i++) {
            Thread childThread= new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println("子線程被執行");
                    
                }
                
            });
            
            childThread.start();
            
        }
        while (Thread.activeCount() > 2) {  //保證前面的線程都執行完
            Thread.yield();
        }
        System.out.println("主線程被執行");
    }
}
複製代碼

執行結果:

子線程被執行
子線程被執行
子線程被執行
子線程被執行
主線程被執行
子線程被執行
複製代碼

爲什麼yield方法會出現這樣的問題?

使當前線程從執行狀態(運行狀態)變爲可執行態(就緒狀態)。cpu會從衆多的可執行態裏選擇,也就是說,當前也就是剛剛的那個線程仍是有可能會被再次執行到的,並非說必定會執行其餘線程而該線程在下一次中不會執行到了。

Java線程中有一個Thread.yield( )方法,不少人翻譯成線程讓步。顧名思義,就是說當一個線程使用了這個方法以後,它就會把本身CPU執行的時間讓掉,讓本身或者其它的線程運行。

打個比方:如今有不少人在排隊上廁所,好不容易輪到這我的上廁所了,忽然這我的說:「我要和你們來個競賽,看誰先搶到廁所!」,而後全部的人在同一塊兒跑線衝向廁所,有多是別人搶到了,也有可能他本身有搶到了。咱們還知道線程有個優先級的問題,那麼手裏有優先權的這些人就必定能搶到廁所的位置嗎? 不必定的,他們只是機率上大些,也有可能沒特權的搶到了。

yield的本質是把當前線程從新置入搶CPU時間的」隊列」(隊列只是說全部線程都在一個起跑線上.並不是真正意義上的隊列)。

5.FutureTast可用於閉鎖,相似於CountDownLatch的做用

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

public class Test5 {
     public static void main(String[] args) {
        MyThread td = new MyThread();
          
        //1.執行 Callable 方式,須要 FutureTask 實現類的支持,用於接收運算結果。
        FutureTask<Integer> result1 = new FutureTask<>(td);
        new Thread(result1).start();
        FutureTask<Integer> result2 = new FutureTask<>(td);
        new Thread(result2).start();
        FutureTask<Integer> result3 = new FutureTask<>(td);
        new Thread(result3).start();
          
        Integer sum;
        try {
                sum = result1.get();
                sum = result2.get();
                sum = result3.get();
                //這裏獲取三個sum值只是爲了同步,並無實際意義
                System.out.println(sum);
        } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
        } catch (ExecutionException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
        }  //FutureTask 可用於 閉鎖 相似於CountDownLatch的做用,在全部的線程沒有執行完成以後這裏是不會執行的
            
        System.out.println("主線程被執行");
           
        }
     
    }
     
    class MyThread implements Callable<Integer> {
     
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            Thread.sleep(1000);
            for (int i = 0; i <= 10; i++) {
                sum += i;
            }
            System.out.println("子線程被執行");
            return sum;
        }
}
複製代碼

6.使用callable+future

Callable+Future最終也是以Callable+FutureTask的形式實現的。 在這種方式中調用了: Future future = executor.submit(task);

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Test6 {
    public static void main(String[] args) throws InterruptedException, ExecutionException { 
        ExecutorService executor = Executors.newCachedThreadPool(); 
        Task task = new Task(); 
        Future<Integer> future1 = executor.submit(task); 
        Future<Integer> future2 = executor.submit(task);
        //獲取線程執行結果,用來同步
        Integer result1 = future1.get();
        Integer result2 = future2.get();
        
        System.out.println("主線程執行");
        executor.shutdown();
        } 
}
class Task implements Callable<Integer>{ 
        @Override public Integer call() throws Exception { 
            int sum = 0; 
            //do something; 
            System.out.println("子線程被執行");
            return sum; 
            }
}
複製代碼

執行結果:

子線程被執行
子線程被執行
主線程執行
複製代碼

補充:

1)CountDownLatch和CyclicBarrier都可以實現線程之間的等待,只不過它們側重點不一樣:

CountDownLatch通常用於某個線程A等待若干個其餘線程執行完任務以後,它才執行;

而CyclicBarrier通常用於一組線程互相等待至某個狀態,而後這一組線程再同時執行;

另外,CountDownLatch是不可以重用的,而CyclicBarrier是能夠重用的。

2)Semaphore其實和鎖有點相似,它通常用於控制對某組資源的訪問權限。

CountDownLatch類其實是使用計數器的方式去控制的,不難想象當咱們初始化CountDownLatch的時候傳入了一個int變量這個時候在類的內部初始化一個int的變量,每當咱們調用countDownt()方法的時候就使得這個變量的值減1,而對於await()方法則去判斷這個int的變量的值是否爲0,是則表示全部的操做都已經完成,不然繼續等待。 實際上若是瞭解AQS的話應該很容易想到可使用AQS的共享式獲取同步狀態的方式來完成這個功能。而CountDownLatch實際上也就是這麼作的。

參考文獻:

blog.csdn.net/u011277123/… blog.csdn.net/joenqc/arti… blog.csdn.net/weixin_3855… blog.csdn.net/LightOfMira… www.cnblogs.com/baizhanshi/…

做者喬戈裏親歷2019秋招,哈工大計算機本碩,百度准入職java工程師,歡迎你們關注個人微信公衆號:程序員喬戈裏

相關文章
相關標籤/搜索