本系列整理Java相關的筆試面試知識點,其他幾篇文章如下: Java筆試面試題整理第八波
Java筆試面試題整理第七波
Java筆試面試題整理第六波
Java筆試面試題整理第五波
Java筆試面試題整理第四波
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>存入:200 pool-1-thread-1>存入:200 pool-1-thread-2>取錢:200 pool-1-thread-1>存入:200 pool-1-thread-2>取錢:200 pool-1-thread-1>存入:200 pool-1-thread-2>取錢:200 pool-1-thread-1>存入:200 pool-1-thread-2>取錢:200 pool-1-thread-2>取錢:200 null
可以看到,列印出來的future.get()擷取的結果為null,這是因為Runnable是沒有傳回值的,需要傳回值要使用Callable,這裡就不再細說了,具體可參考如下文章: http://blog.csdn.net/xiaojin21cen/article/details/41820983
http://icgemu.iteye.com/blog/467848
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有一個缺陷: