1. 程式人生 > >Java筆試面試題整理第六波(修正版)

Java筆試面試題整理第六波(修正版)

本系列整理Java相關的筆試面試知識點,其他幾篇文章如下:

1、執行緒池ThreadPool相關

在java.util.concurrent包下,提供了一系列與執行緒池相關的類。合理的使用執行緒池,可以帶來多個好處:(1)降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗;(2)提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行;(3)提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。執行緒池可以應對突然大爆發量的訪問,通過有限個固定執行緒為大量的操作服務,減少建立和銷燬執行緒所需的時間。
與執行緒執行、執行緒池相關類的關係如圖:
我們一般通過工具類Executors的靜態方法(如newFixedThreadPool())來獲取ThreadPoolExecutor執行緒池或靜態方法(如newScheduledThreadPool())來獲取ScheduleThreadPoolExecutor執行緒池。如下使用:ExecutorService threadpool= Executors.newFixedThreadPool(10);我們指定了獲取10個數量的固定執行緒池,Executors中有很多過載的獲取執行緒池的方法,比如可以通過自定義的ThreadFactory來為每個創建出來的Thread設定更為有意義的名稱。Executors建立執行緒池的方法內部也就是new出新的ThreadPoolExecutor或ScheduleThreadPoolExecutor,給我們配置了很多預設的設定。如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
上面通過ThreadPoolExecutor的構造方法,為我們建立了一個執行緒池,很多引數Executors工具類自動為我們配置好了。建立一個ThreadPoolExecutor執行緒池一般需要以下幾個引數:
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 
(1)corePoolSize(執行緒池的基本大小):當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使其他空閒的基本執行緒能夠執行新任務也會建立執行緒,等到需要執行的任務數大於執行緒池基本大小時就不再建立。如果呼叫了執行緒池的prestartAllCoreThreads方法,執行緒池會提前建立並啟動所有基本執行緒。(2)maximumPoolSize(執行緒池最大大小):執行緒池允許建立的最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是如果使用了無界的任務佇列這個引數就沒什麼效果。(3)keepAliveTime(執行緒活動保持時間):執行緒池的工作執行緒空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高執行緒的利用率。(4)TimeUnit(執行緒活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS)等。(5)workQueue(任務佇列):用於儲存等待執行的任務的阻塞佇列。 可以選擇以下幾個阻塞佇列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue(6)threadFactory:用於設定建立執行緒的工廠,可以通過執行緒工廠給每個創建出來的執行緒設定更有意義的名字。(7)handler(飽和策略):當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是AbortPolicy,表示無法處理新任務時丟擲異常。我們儘量優先使用Executors提供的靜態方法來建立執行緒池,如果Executors提供的方法無法滿足要求,再自己通過ThreadPoolExecutor類來建立執行緒池提交任務的兩種方式:(1)通過execute()方法,如:
ExecutorService threadpool= Executors.newFixedThreadPool(10);
threadpool.execute(new Runnable(){...});
這種方式提交沒有返回值,也就不能判斷任務是否被執行緒池執行成功(2)通過submit()方法,如:
Future<?> future = threadpool.submit(new Runnable(){...});
    try {
            Object res = future.get();
        } catch (InterruptedException e) {
            // 處理中斷異常
            e.printStackTrace();
        } catch (ExecutionException e) {
            // 處理無法執行任務異常
            e.printStackTrace();
        }finally{
            // 關閉執行緒池
            executor.shutdown();
        }
使用submit 方法來提交任務,它會返回一個Future物件,通過future的get方法來獲取返回值,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間後立即返回,這時有可能任務沒有執行完。執行緒池工作流程分析:(來自參考文章)
從上圖我們可以看出,當提交一個新任務到執行緒池時,執行緒池的處理流程如下:
1、首先執行緒池判斷基本執行緒池是否已滿(< corePoolSize ?)?沒滿,建立一個工作執行緒來執行任務。滿了,則進入下個流程。
2、其次執行緒池判斷工作佇列是否已滿?沒滿,則將新提交的任務儲存在工作佇列裡。滿了,則進入下個流程。3、最後執行緒池判斷整個執行緒池是否已滿(< maximumPoolSize ?)?沒滿,則建立一個新的工作執行緒來執行任務,滿了,則交給飽和策略來處理這個任務。也就是說,執行緒池優先要創建出基本執行緒池大小(corePoolSize)的執行緒數量,沒有達到這個數量時,每次提交新任務都會直接建立一個新執行緒,當達到了基本執行緒數量後,又有新任務到達,優先放入等待佇列,如果佇列滿了,才去建立新的執行緒(不能超過執行緒池的最大數maxmumPoolSize)關於執行緒池的配置原則可閱讀參考文章。ThreadPoolExecutor簡單例項:
public class BankCount {
    public synchronized void addMoney(int money){//存錢
        System.out.println(Thread.currentThread().getName() + ">存入:" + money);
    }

    public synchronized void getMoney(int money){//取錢
        System.out.println(Thread.currentThread().getName() + ">取錢:" + money);
    }
}
測試類:
public class BankTest {
    public static void main(String[] args) {
        final BankCount bankCount = new BankCount();

        ExecutorService executor = Executors.newFixedThreadPool(10);
        executor.execute(new Runnable() {//存錢執行緒
            @Override
            public void run() {
                int i = 5;
                while(i-- > 0){
                    bankCount.addMoney(200);
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Future<?> future = executor.submit(new Runnable() {//取錢執行緒
            @Override
            public void run() {
                int i = 5;
                while(i-- > 0){
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    bankCount.getMoney(200);
                }
            }
        });
        try {
            Object res = future.get();
            System.out.println(res);
        } catch (InterruptedException e) {
            // 處理中斷異常
            e.printStackTrace();
        } catch (ExecutionException e) {
            // 處理無法執行任務異常
            e.printStackTrace();
        }finally{
            // 關閉執行緒池
            executor.shutdown();
        }
    }
}
列印結果如下:pool-1-thread-1>存入:200pool-1-thread-1>存入:200pool-1-thread-2>取錢:200pool-1-thread-1>存入:200pool-1-thread-2>取錢:200pool-1-thread-1>存入:200pool-1-thread-2>取錢:200pool-1-thread-1>存入:200pool-1-thread-2>取錢:200pool-1-thread-2>取錢:200null可以看到,打印出來的future.get()獲取的結果為null,這是因為Runnable是沒有返回值的,需要返回值要使用Callable,這裡就不再細說了,具體可參考如下文章:

2、生產者和消費者模型

生產者消費者模型,描述是:有一塊緩衝區作為倉庫,生產者可以將產品放入倉庫,消費者可以從倉庫中取走產品。解決消費者和生產者問題的核心在於保證同一資源被多個執行緒併發訪問時的完整性。一般採用訊號量或加鎖機制解決。下面介紹Java中解決生產者和消費者問題主要三種仿:(1)wait() / notify()、notifyAll()wait和notify方法是Object的兩個方法,因此每個類都會擁有這兩個方法。wait()方法:使當前執行緒處於等待狀態,放棄鎖,讓其他執行緒執行notify()方法喚醒其他等待同一個鎖的執行緒,放棄鎖,自己處於等待狀態。如下例子:
/**
 * 倉庫
 */
public class Storage {
    private static final int MAX_SIZE = 100;//倉庫的最大容量
    private List<Object> data = new ArrayList<Object>();//儲存載體
    /**
     * 生產操作
     */
    public synchronized void produce(int num){
        if(data.size() + num > MAX_SIZE){//如果生產這些產品將超出倉庫的最大容量,則生產操作阻塞
            System.out.println("生產操作-->數量:" + num + ",超出倉庫容量,生產阻塞!------庫存:" + data.size());
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //到這裡,表示可以正常生產產品
        for(int i = 0; i < num; i++){//生產num個產品
            data.add(new Object());
        }
        System.out.println("生產操作-->數量:" + num + ",成功入庫~------庫存:" + data.size());
        //生產完產品後,喚醒其他等待消費的執行緒
        notify();
    }

    /**
     * 消費操作
     */
    public synchronized void consume(int num){
        if(data.size() - num < 0){//如果產品數量不足
            System.out.println("消費操作-->數量:" + num + ",庫存不足,消費阻塞!------庫存:" + data.size());
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //到這裡,表示可以正常消費
        for(int i = 0; i < num; i++){//消費num個產品
            data.remove(0);
        }
        System.out.println("消費操作-->數量:" + num + ",消費成功~------庫存:" + data.size());
        //消費完產品後,喚醒其他等待生產的執行緒
        notify();
    }

}
生產者:
public class Producer implements Runnable{
    private Storage storage;
    private int num;//每次生產多少個

    public Producer(Storage sto,int num){
        storage = sto;
        this.num = num;
    }

    @Override
    public void run() {
        storage.produce(num);
    }
}
消費者:
public class Consumer implements Runnable{
    private Storage storage;
    private int num;//每次消費多少個

    public Consumer(Storage sto,int num){
        storage = sto;
        this.num = num;
    }

    @Override
    public void run() {
        storage.consume(num);
    }
}
測試類:
public class StorageTest {
    public static void main(String[] args) {
        Storage storage = new Storage();
        ExecutorService taskSubmit = Executors.newFixedThreadPool(10);    //來使用使用上一節我們總結的執行緒池知識

        //給定4個消費者
        taskSubmit.submit(new Consumer(storage, 30));
        taskSubmit.submit(new Consumer(storage, 10));
        taskSubmit.submit(new Consumer(storage, 20));

        //給定6個生產者
        taskSubmit.submit(new Producer(storage, 70));
        taskSubmit.submit(new Producer(storage, 10));
        taskSubmit.submit(new Producer(storage, 20));
        taskSubmit.submit(new Producer(storage, 10));
        taskSubmit.submit(new Producer(storage, 10));
        taskSubmit.submit(new Producer(storage, 10));

        taskSubmit.shutdown();
    }
}
列印結果:消費操作-->數量:30,庫存不足,消費阻塞!------庫存:0生產操作-->數量:10,成功入庫~------庫存:10生產操作-->數量:70,成功入庫~------庫存:80生產操作-->數量:10,成功入庫~------庫存:90生產操作-->數量:10,成功入庫~------庫存:100生產操作-->數量:20,超出倉庫容量,生產阻塞!------庫存:100消費操作-->數量:10,消費成功~------庫存:90生產操作-->數量:20,成功入庫~------庫存:110生產操作-->數量:10,超出倉庫容量,生產阻塞!------庫存:110消費操作-->數量:20,消費成功~------庫存:90消費操作-->數量:30,消費成功~------庫存:60生產操作-->數量:10,成功入庫~------庫存:70在倉庫中,喚醒我們使用的是notify()而沒有使用notifyAll(),是因為在這裡,如果測試資料設定不當很容易造成死鎖(比如一下喚醒了所有的生產程序),因為使用wait和notify有一個缺陷邏輯本應該要這樣設計的,在produce()操作後,只要喚醒等待同一把鎖的消費者程序,在consume()後,喚醒等待同一把鎖的生產者程序,而notify()或notifyAll()將生產者和消費者執行緒都喚醒了。下面的第二種方法可以解決這個問題。wait和notify在“消費者和生產者”問題上也很有用,比如,在A類的某個方法中呼叫了傳進來的B物件的一個方法,A類方法的後面程式碼依賴於剛剛呼叫的B的返回值,但是B物件的這個方法是一個非同步的操作,此時就可以在A方法中呼叫完B物件的方法後自我阻塞,即呼叫wait()方法,而在B物件的那個方法中,待非同步操作完成後,呼叫notify(),喚醒處於等待同一鎖物件的執行緒。如下:A類的某個方法中:
XmppManager xmppManager = notificationService.getXmppManager();
                if(xmppManager != null){
                    if(!xmppManager.isAuthenticated()){
                        try {
                                synchronized (xmppManager) {//等待客戶端連線認證成功
                                Log.d(LOGTAG, "wait for authenticated...");
                                xmppManager.wait();
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    //執行到此處,說明是認證成功的,有兩種可能,一是執行速度很快呼叫notificationService.getXmppManager()後直接返回了結果,二是B中處理完了呼叫notify方法    Log.d(LOGTAG, "authenticated already. send SetTagsIQ now...");B中處理完後:    //客戶端連線認證成功後,喚醒擁有xmppManager鎖的物件     synchronized (xmppManager) {           xmppManager.notifyAll();     }(2)await() / signal()
在JDK1.5之引入concurrent包之後,新引入了await()和signal()方法來做同步,功能和wait()和notify()方法相同,可以完全取代,但await()和signal()需要和Lock機制(關於Lock機制前面已總結)結合使用,更加靈活。正如第一種所說,可以通過呼叫Lock的newCondition()方法依次獲取兩個條件變數,一個針對倉庫空的,一個針對倉庫滿的條件變數,通過新增變數進行同步控制。修改倉庫類Storage:
/**
 * 倉庫
 */
public class Storage {
    private static final int MAX_SIZE = 100;//倉庫的最大容量
    private List<Object> data = new ArrayList<Object>();//儲存載體

    private Lock lock = new ReentrantLock();//可重入鎖
    private Condition full = lock.newCondition();//倉庫滿的條件變數
    private Condition empty = lock.newCondition();//倉庫空時的條件變數

    /**
     * 生產操作
     */
    public void produce(int num){
        lock.lock();    //加鎖
        if(data.size() + num > MAX_SIZE){//如果生產這些產品將超出倉庫的最大容量,則生產操作阻塞
            System.out.println("生產操作-->數量:" + num + ",超出倉庫容量,生產阻塞!------庫存:" + data.size());
            try {
                full.await();    //阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //到這裡,表示可以正常生產產品
        for(int i = 0; i < num; i++){//生產num個產品
            data.add(new Object());
        }
        System.out.println("生產操作-->數量:" + num + ",成功入庫~------庫存:" + data.size());
        //生產完產品後,喚醒其他等待消費的執行緒
        empty.signalAll();

        lock.unlock();    //釋放鎖
    }

    /**
     * 消費操作
     */
    public void consume(int num){
        lock.lock();    //加鎖
        if(data.size() - num < 0){//如果產品數量不足
            System.out.println("消費操作-->數量:" + num + ",庫存不足,消費阻塞!------庫存:" + data.size());
            try {
                empty.await();    //阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //到這裡,表示可以正常消費
        for(int i = 0; i < num; i++){//消費num個產品
            data.remove(0);
        }
        System.out.println("消費操作-->數量:" + num + ",消費成功~------庫存:" + data.size());
        //消費完產品後,喚醒其他等待生產的執行緒
        full.signalAll();

        lock.unlock();    //釋放鎖
    }
}
列印結果:消費操作-->數量:30,庫存不足,消費阻塞!------庫存:0消費操作-->數量:10,庫存不足,消費阻塞!------庫存:0消費操作-->數量:20,庫存不足,消費阻塞!------庫存:0生產操作-->數量:70,成功入庫~------庫存:70生產操作-->數量:10,成功入庫~------庫存:80生產操作-->數量:10,成功入庫~------庫存:90生產操作-->數量:10,成功入庫~------庫存:100生產操作-->數量:10,超出倉庫容量,生產阻塞!------庫存:100消費操作-->數量:30,消費成功~------庫存:70消費操作-->數量:10,消費成功~------庫存:60消費操作-->數量:20,消費成功~------庫存:40生產操作-->數量:10,成功入庫~------庫存:50生產操作-->數量:20,成功入庫~------庫存:70使用await和signal後,加鎖解鎖操作就交給了Lock,不用再使用synchronized同步(具體可看前面總結的同步的實現方法),在produce中滿倉後阻塞,生產完後喚醒等待的消費執行緒,consume中庫存不足後阻塞,消費完後喚醒等待的生產者執行緒,表示可以消費了(3)BlockingQueue阻塞佇列方式
在上一節關於執行緒池的總結中,我們看到了要建立一個執行緒池如ThreadPoolExecutor,需要傳入一個任務佇列即BlockingQueue,BlockingQueue(介面)用於儲存等待執行的任務的阻塞佇列。 可以選擇以下幾個阻塞佇列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。    >ArrayBlockingQueue:是一個基於陣列結構的有界阻塞佇列,此佇列按 FIFO(先進先出)原則對元素進行排序。    >LinkedBlockingQueue:一個基於連結串列結構的阻塞佇列,此佇列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列。    >SynchronousQueue:一個不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列。    >PriorityBlockingQueue:一個具有優先順序的無限阻塞佇列。BlockingQueue的所有實現類內部都是已經實現了同步的佇列,實現的方式採用的是上面介紹的第二種await()/signal() + Lock同步的機制。在生成阻塞佇列時,可以指定佇列大小。用於阻塞操作的方法主要為:    put()方法:插入一個元素,如果超過容量則自我阻塞,等待喚醒;    take()方法:取走一個元素,如果容量不足了,自我阻塞,等待喚醒;put和take內部自己實現了await和signal、lock的機制處理,不再需要我們做相應操作。修改Storage程式碼如下:
public class Storage {
    private static final int MAX_SIZE = 100;//倉庫的最大容量
    private BlockingQueue<Object> data = new LinkedBlockingQueue<Object>(MAX_SIZE);    //使用阻塞佇列作為儲存載體
    /**
     * 生產操作
     */
    public void produce(int num){
        if(data.size() == MAX_SIZE){//如果倉庫已達最大容量
            System.out.println("生產操作-->倉庫已達最大容量!");
        }
        //到這裡,表示可以正常生產產品
        for(int i = 0; i < num; i++){//生產num個產品
            try {
                data.put(new Object());    //put內部自動實現了判斷,超過最大容量自動阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("生產操作-->數量:" + num + ",成功入庫~------庫存:" + data.size());
    }

    /**
     * 消費操作
     */
    public void consume(int num){
        if(data.size() ==  0){//如果產品數量不足
            System.out.println("消費操作--庫存不足!");
        }
        //到這裡,表示可以正常消費
        for(int i = 0; i < num; i++){//消費num個產品
            try {
                data.take();    //take內部自動判斷,消耗後庫存是否充足,不足自我阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消費操作-->數量:" + num + ",消費成功~------庫存:" + data.size());
    }
}
列印結果:消費操作--庫存不足!消費操作--庫存不足!消費操作--庫存不足!生產操作-->數量:70,成功入庫~------庫存:45消費操作-->數量:30,消費成功~------庫存:45生產操作-->數量:10,成功入庫~------庫存:56生產操作-->數量:20,成功入庫~------庫存:75生產操作-->數量:10,成功入庫~------庫存:85生產操作-->數量:10,成功入庫~------庫存:89消費操作-->數量:10,消費成功~------庫存:60生產操作-->數量:10,成功入庫~------庫存:70消費操作-->數量:20,消費成功~------庫存:70可以看到,Storage中produce和consume方法中我們直接通過put和take方法往容器中新增或移除產品即可,沒有進行邏輯控制(其實上面兩個方法中if都可以去掉,只是為了列印效果才加上的),這是因為BlockingQueue內部已經實現了,不需要我們再次控制。同時,我們看到列印的庫存資訊出現了不匹配,這個主要是因為我們的列印語句Systm.out.println()沒有被同步導致的,因為同步語句只是在put和take方法內部,而我們列印語句中使用了data這個共享變數。這裡因為我們需要看效果,所以才加的列印語句,並不影響我們對BlockingQueue的使用。因此,在Java中,使用BlockingQueue阻塞佇列的方式可以很方便的為我們處理生產者消費則問題推薦使用在我們的程式設計生涯中,我們自己要去寫生產者和消費者問題,多是前面第一種介紹的“類似消費者生產者問題”上。解決生產者和消費者問題還有管道的方式,即在生產者和消費者之間建立一個管道緩衝區,Java中用PipedInputStream / PipedOutputStream實現,由於這種方式對於傳輸物件不易封裝,因此實用性不高,就不具體介紹了。

3、sleep和wait的區別

sleep是Thread的靜態方法,wait是Object的方法。兩個方法都會暫停當前執行緒(1)sleep使當前執行緒阻塞讓出CPU,給其他執行緒執行的機會;如果當前執行緒擁有鎖,不會釋放鎖,也即“睡著我也要擁有鎖”。睡眠時間一到,進入就緒狀態,如果當前CPU空閒,才會繼續執行。(2)wait方法呼叫後,當前執行緒進入阻塞狀態,進入到和該物件(即誰呼叫了wait()方法,如list.wait())相關的等待池中。,讓出CPU,給其他執行緒執行的機會;當超時間過了或者別的執行緒呼叫了notify()或notifyAll()方法時才會喚醒當前等待同一把鎖的執行緒。(3)wait方法必須要放在同步塊中,如syncbronized或Lock同步中所以sleep和wait的主要區別是:sleep:保持鎖,睡眠時間到進入就緒狀態;wait:釋放鎖,等待其他執行緒的notify操作或超時喚醒。參考文章: