使用線程池的目的
降低資源消耗
通過重複利用已創建的線程降低線程創建和銷燬造成的消耗
提高響應效率
當任務到達時,任務可以不需要等到線程創建就能立即執行
方便管理
線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用。
提交一個任務到線程池中,線程池的處理流程如下:
1、判斷線程池裏的核心線程是否都在執行任務,如果不是(核心線程空閒或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。
2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列裏。如果工作隊列滿了,則進入下個流程。
3、判斷線程池裏的線程是否都處於工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。
通過Executors(JDK1.5併發包)四種創建方式
newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
newFixedThreadPool創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newScheduledThreadPool創建一個定長線程池,支持定時及週期性任務執行。
schedule(Runable command, long delay, TimeUnit unit):定時開始執行
scheduleAtFixedRate(Runable command, long initialDelay, long period, TimeUnit unit): 第一次定時開始執行之後,週期性的繼續執行(週期頻率爲:上一次任務開始執行時間爲起點,過了period時間,調度下一次任務,當然如果上一次任務執行還沒結束,就算到了period時間也不會執行下一次任務)。
scheduleWithFixedFixedDelay(Runable command, long initialDelay, long delay, TimeUnit unit): 第一次定時開始執行之後,週期性的繼續執行。(週期頻率爲:上一個任務結束後,經過delay時間再進行下一次任務調度)。
newSingleThreadExecutor創建一個單線程話的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO,LIFO,優先級)執行。
ThreadPoolExecutor上述4中方法實際上底層實現是ThreadPoolExecutor的4個構造器。
Executors的submit方法
以submit(Callable task)方法開起的線程可以獲得線程返回值,通過get()獲得當前線程的返回值,當獲取返回值得時候如果這個線程的寫操作還沒有完成則會等待(通常建立一個新的子線程來獲取,這樣不會阻塞主線程)。在獲取該線程的返回值之前,是不會影響其他線程的執行的。
底層實現:future模式(以後說)
shutdown()
關閉線程池,不會暴力關閉,而是等待所有任務執行完成後再關閉。只是發送了一個關閉信號,shutdown()方法執行後,這個線程池不會再接收新的任務。
配置線程池
CPU密集
該任務執行的時候,不會產生大量IO阻塞,CPU運行速度快
配置最大線程數=CPU核數
IO密集
該任務需要大量I/O操作,產生阻塞,如果是在單線程中執行,可以使用多線程技術,CPU運算能力不會浪費等待資源
配置最大線程數:2*CPU核數
CPU的數量*CPU使用率*(1+等待時間/計算時間)
CPU數量可以通過java中的Runtime.getRuntime().availableProcessors()方法獲取。
併發包
stop(棄用)
原因:暴力終止線程,並釋放所持有的鎖,可能會造成保存的數據不一致,而導致數據永久地破壞和丟失
interrupt()
中斷線程
isInterrupted()
判斷是否被中斷
interrupted()
判斷是否被中斷,並清除當前中斷狀態
sleep(xx)
阻塞(xx毫秒後重新進入就緒狀態),會由於中斷而拋出異常,此時,它會清除中斷標記。所以我們可以在catch塊中通過interrupt()再次設置中斷標記爲,就可以防止下次循環無法捕獲這個中斷的問題
鎖對象.wait()
讓線程等待,釋放鎖的資源
鎖對象.notify()
喚醒當前對象鎖池等待的線程
join(x)
讓目標程序先執行,執行結束之後再執行自己(x可選,填的話就是設置最大等待時長,超過這個時長將會回到自己繼續執行)
本質是讓調用線程wait()再當前線程對象實例上
yield()
讓出CPU讓別人先執行,之後再重新爭搶CUP。(Thread.yeild())
CountDownLatch
倒計時器:new CountDownLatch(x); x表示計數個數。CountDownLatch,await*(方法,說明x個任務全部完成後,才能繼續執行
CyclicBarrier
循環柵欄:可反覆使用的計數器,當計數任務完成後,進行下一次計數
特有的異常類:BrokenBarrierException表示CyclicBarrier已經破損,無需繼續等待
信號量
允許幾個線程同時訪問
Semaphore類
通過new Semaphore(x)創建對象,“x"表示允許幾個線程同時訪問
acquire()申請信號量,無法獲得會等待,直到有線程釋放信號量
acquireUniterruptibly()和acquire()類似,但是不響應中斷
tryAcquire()嘗試獲得一個許可,成功返回true失敗返回false,不會進行等待
release()釋放信號量
阻塞工具類:LockSupport
park():掛起線程(類似suspend())
unpark():重啓線程(類似resume())
拒絕策略:均實現了RejectedExecutionHandler接口,若這些策略扔無法滿足應用,則可以自己擴展RejectedExecutionHandler接口(如重寫beforeExecute()(任務開始前),afterExecute()(任務結束後)、terminiated()(整個線程池退出)等)
AbortPolicy
直接拋出異常,阻止系統正常工作
GallerRumsPolicy
只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。不會真的丟棄任務,但是,任務提交線程的性能極有可能會急劇下降
DiscardOledestPolicy
丟棄最老的一個請求,也就是即將被執行的一個任務,並嘗試再次提交當前任務
DiscardPolicy
丟棄無法處理的任務,不予任何處理。(任務丟失)
一定要在synchronized裏執行,持有同一把鎖。
線程池的優化
提交的任務和線程數量並不是一對一的關係。絕大多數情況下,一個物理線程實際上是需要處理多個邏輯任務的。因此每個線程必然需要擁有一個任務隊列。在實際執行過程中,可能會出現這麼一種情況:線程A已經把自己的任務都執行完成了,而線程B還有一堆任務等着處理,此時線程A就會“幫助”線程B,從線程B的隊列中拿一個任務過來處理,儘可能地達到平衡。一個值得注意的地方是,當線程試圖幫助別人時,總是從任務隊列的底部開始拿數據,而線程試圖執行自己的任務時,則是從相反的頂部開始拿。因此這種行爲也十分有利於避免數據競爭。
異常處理,幫助我們定位到拋異常的地方:
public class TraceThreadPoolExecutor extends ThreadPoolExecutor{
public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue){
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable task) {
super.execute(wrap(task, clientTrace(), Thread.currentThread().getName()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
}
private Exception clientTrace(){
return new Exception("Client stack trace");
}
private Runnable wrap(final Runnable task, final Exception clientStack, String clientTreadName){
return new Runnable() {
@Override
public void run() {
try{
task.run();
}catch (Exception e){
clientStack.printStackTrace();
throw e;
}
}
};
}
}
threadlocal
給每個線程提供一個局部變量,但是不是通過threadLocal來完成的,而是需要在應用層面上來保證的,如果在應用上爲每一個線程分配了相同的對象實例,那麼ThreadLocal也不能保證線程安全。
當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
4個方法
void set(Object value)
public Object get()
public void remove()
protected Object initialValue()
創建threadLocal:
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
};
};
底層就是一個ThreadLocalMap可以理解爲一個map集合,key是當前線程
內存泄露
當我們使用線程池,意味着當前線程未必會退出。我們將一些對象設置到ThreadLocal中(它實際保存在線程持有的ThreadLocalMap內),可能會使系統出現內存泄露的問題(用完未清理),使用完了再也不用了,但是卻無法被回收。ThreadLocalMap更加類似WeakHashMap,它的實現使用了弱引用,java虛擬機在垃圾回收時,如果發現弱引用,就會立即回收
解決辦法:使用ThreadLocal.remove()方法將變量移除,或者直接賦值null,那麼這個對象會更容易被垃圾回收器發現,從而加速回收。(如:t = null,那麼這個ThreadLocal對應的所有線程的局部變量都有可能被回收)。
參考書籍《JAVA高併發程序設計》