線程的未捕獲異常與監控
jdk1.5引入UncaughtExceptionHandler接口,當線程提前被異常終止,則會回調該接口中的方法,例:
package JavaCoreThreadPatten.capter08;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 通過自定義線程的Thread.UncaughtExceptionHandler,當線程發生異常退出後運行業務邏輯,保障我們的業務流程
*/
public class ThreadMonitorDemo {
volatile boolean inited = false;
private static int threadIndex = 0;
final BlockingQueue<String> channel = new ArrayBlockingQueue<String>(1000);
public static void main(String[] args) throws InterruptedException {
ThreadMonitorDemo threadMonitorDemo = new ThreadMonitorDemo();
threadMonitorDemo.init();
for(int i=0;i<1000;i++){
threadMonitorDemo.service("test:"+i);
}
}
public synchronized void init(){
if(inited){
return;
}
WorkThread workThread = new WorkThread();
workThread.setName("workThread:"+threadIndex++);
workThread.setUncaughtExceptionHandler(new ThreadMonitor());
workThread.start();
inited=true;
}
public void service(String message){
try {
channel.put(message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 檢測到線程異常結束,則進行後續處理
*/
private class ThreadMonitor implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
String threadInfo = t.getName();
System.err.println("攔截到線程:"+threadInfo+"拋出異常");
//重新執行
System.err.println("不能影響,繼續運行");
WorkThread workThread = new WorkThread();
workThread.setName("workThread:"+threadIndex++);
workThread.setUncaughtExceptionHandler(new ThreadMonitor());
workThread.start();
}
}
class WorkThread extends Thread{
@Override
public void run() {
for(;;){
try {
String msg = channel.take();
process(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void process(String message){
double s = Math.random()*100;
System.out.println(s);
if(s<15){
throw new RuntimeException("test");
}
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(3));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
線程工程
自定義線程工廠,例:
package JavaCoreThreadPatten.capter08;
import java.util.Objects;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;
/**
* 自定義線程工廠,在創建線程的過程中,指定線程的異常終結處理方法
*/
public class MyThreadFactory implements ThreadFactory {
/**
* 線程名稱前綴
*/
private String namePrefix;
/**
* 記錄線程數量
*/
private final AtomicLong atomicLong = new AtomicLong(1);
public MyThreadFactory (String namePrefix){
this.namePrefix = namePrefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = doMakeThread(r);
t.setUncaughtExceptionHandler(new ExceptionCause());
if(t.isDaemon()){
t.setDaemon(false);
}
if(t.getPriority() != Thread.NORM_PRIORITY){
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
protected Thread doMakeThread(Runnable r){
Thread t = new Thread(r,"Thread "+atomicLong.getAndIncrement()+" From MyThreadFactory"){
@Override
public String toString() {
ThreadGroup threadGroup = this.getThreadGroup();
String groupName = Objects.nonNull(threadGroup) ? threadGroup.getName() : "";
return "Thread:"+groupName+" "+this.getName()+" from MyThreadFactory";
}
};
return t;
}
/**
* 線程異常終結處理類
*/
private class ExceptionCause implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
//記錄線程異常終結的錯誤信息
System.err.println("線程:"+t+"----異常終結");
}
}
}
線程的高效利用:線程池
線程是一種昂貴的資源:
- 線程的創建與啓動的開銷。與普通的對象相比,java線程還佔用了額外的存儲空間--棧空間。並且,線程的啓動會產生相應的線程調度開銷。
- 線程的銷燬。線程的銷燬也有其開銷。
- 線程調度的開銷。線程的調度會導致上下文切換,從而增加處理器資源的消耗,使得應用程序本身可以使用的處理器資源減少。
- 一個系統能夠同時運行的線程數量總是受限於該系統所擁有的的處理器數目。無論是CPU密集型還是IO密集型線程,這些線程的數量的臨界值總是處理器的數量。
線程池內部可以預先創建一定數量的工作者線程,客戶端代碼並不需要向線程池借用線程而是將其需要執行的任務作爲一個對象提交給線程池,線程池可能將這些任務緩存在隊列(工作隊列)之中,而線程池內部的各個工作線程則不斷的從隊列中取出任務並執行任務。
java.util.concurrent.ThreadPoolExecutor類就是一個線程池,調用ThreadPoolExecutor.submit方法提交任務。
ThreadPoolExecutor的線程池大小有三種狀態:當前線程池大小(current pool size)標識線程池中實際工作者線程的數量;最大線程池大小(maximum pool size)表示線程池中允許存在的工作者線程的數量上限,核心線程大小(core pool size)表示一個不大於最大線程池大小的工作者線程數量上限。
其中,workQueu是被稱爲工作隊列的阻塞隊列,它相當於生產者-消費者模式中的傳輸通道,corePoolSize用於指定線程池核心數大小,maximumPoolSize用於指定最大線程池大小。keepAliveTime和unit合在一起用於指定線程池中空閒線程的最大存活時間。threadFactory指定用於創建工作者線程的線程工廠。
在初始狀態下,客戶端每提交一個任務線程池就創建一個工作者線程來處理該任務。隨着客戶端不斷地提交任務,當前線程池大小也相應增加。在當前線程池大小達到核心線程池大小的時候,新來的任務會被存入工作隊列之中。這些緩存的任務由線程池中的所有工作者線程負責取出進行執行。線程池將任務存入工作隊列的時候調用的是BlockingQueue的非阻塞方法offer(E e),通過該方法的源碼註釋我們可以知道該方法當隊列已經滿的時候,不會拋出異常也不會阻塞,只會返回成功還是失敗:
因此工作隊列滿並不會使提交任務的客戶端線程暫停。當工作隊列滿的時候,線程池會繼續創建新的工作者線程,直到當前線程池大小達到最大線程池大小。線程池是通過調用threadFactory.newThread方法來創建工作者線程的。若不指定線程工廠,則默認使用Executors.defaultThreadFactory()所返回的默認線程工廠。當線程池飽和時,即工作者隊列滿且當前線程池大小達到了最大線程池大小的情況下,客戶端試圖提交的任務會被拒絕(reject)。爲了提高線程池的可靠性,java標準庫引入了一個RejectedExecutionHandler接口用於封裝被拒絕的任務的處理策略。
其中,r代表被拒絕的任務,executor代表拒絕任務r的線程池實例。我們可以通過創建線程池實例或者線程池的setRejectedExecutionHandler(RejectedExecutionHandler)方法來爲線程池關聯一個RejectedExecutionHandler。當任務被拒絕,則會調用RejectedExecutionHandler中的默認方法,jdk中默認給我們提供了四種拒絕任務後的執行策略:
在當前線程池大小超過線程池核心大小的時候,超過線程池核心大小部分的工作者線程空閒時間達到keepAliveTime所指定的時間後就會被清理掉。【時間長度過長過短都會對性能產生影響】
線程池中數量上等於核心線程池大小的那部分工作者線程,是在接收到執行任務後創建的,如果需要提升響應時間可以調用ThreadPoolExecutor.prestartAllCoreThreads()使線程池在未接收到任何任務的情況下預先創建並啓動所有核心線程。
ThreadPoolExecutor.shutdown()/shutdownNow()方法可以用來關閉線程池。使用shutdown()關閉線程池的時候,已提交的任務會被繼續執行,而新提交的任務會像線程池飽和時那樣被拒絕掉。ThreadPoolExecutor.shutdown()返回的時候線程池可能尚未關閉,即線程池中可能還有工作者線程正在執行任務。應用代碼可以通過調用ThreadPoolExecutor.awaitTermination(long timeout,TimeUnit unit)來等待線程池關閉結束。使用ThreadPoolExecutor.shutdownNow()關閉線程池的時候,正在執行的任務會被停止,已提交而等待執行的任務也不會被執行。該方法的返回值是已提交而未被執行的任務列表,這爲被取消的任務的重試提供了一個機會。由於ThreadPoolExecutor.shutdownNow()內部是通過調用工作者線程的interrupt方法來停止正在執行的任務的,因此某些無法響應中斷的任務可能永遠也不會停止。
線程池的核心任務數如果是I/O密集型任務,那麼最大線程數一般可以設置爲處理器數目的兩倍,如果是計算密集型,那麼可以與處理器數量一致。
任務的處理結果、異常處理與取消
Callable接口相當於一個增強型的Runnable接口,call方法的返回值代表相應任務的處理結果。
Executors.callable(Runnable task,T result)能夠將Runnable接口轉換成Callable接口實例。
Future接口實例可以被看做提交給線程池執行的任務的處理結果句柄,Future.get()方法可以用來獲取task參數所指定的任務的處理結果,該方法會使當前線程暫停,直到相應的任務執行結束。
由於在任務未執行完畢的情況下調用Future.get()方法來獲取該任務的處理結果會導致等待並由此導致上下文切換,因此客戶端代碼應該儘可能早地向線程池提交任務,並儘可能晚的調用Future.get()方法來獲取任務的處理結果,而線程池正好利用這段時間來執行已提交的任務。
客戶端代碼應該儘可能早的向線程池提交任務,並僅在需要相應任務的處理結果數據的那一刻才調用Future.get()方法。
package JavaCoreThreadPatten.capter08;
import java.io.File;
import java.util.Random;
import java.util.concurrent.*;
/**
* 線程池
*/
public class TaskResultRetrievalDemo {
private static final int processNum = Runtime.getRuntime().availableProcessors();
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(0,
processNum, 5, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy());
public static void main(String[] args) throws InterruptedException {
TaskResultRetrievalDemo taskResultRetrievalDemo = new TaskResultRetrievalDemo();
Future<String> future = taskResultRetrievalDemo.recognizeImage("hshhshs");
TimeUnit.SECONDS.sleep(2);
try {
future.get(10,TimeUnit.SECONDS);
} catch (ExecutionException e) {
} catch (TimeoutException e) {
e.printStackTrace();
}
}
public Future<String> recognizeImage(final String imageFile) {
return THREAD_POOL_EXECUTOR.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return doSomeService(new File(imageFile));
}
});
}
protected String doSomeService(File imageFile) {
//模擬耗時操作
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
} catch (InterruptedException e) {
}
return "success";
}
}
線程池死鎖
如果線程池中執行的任務在其執行過程中又會向同一個線程池提交另外一個任務,而前一個任務的執行結束又依賴於後一個任務的執行結果,那麼就有可能出現線程池死鎖現象。適合提交給同一線程池實例執行的任務是相互獨立的任務,而不是彼此有依賴關係的任務。
同一個線程池只能用於執行相互獨立的任務,彼此有依賴關係的任務需要提交給不同的線程池執行以避免死鎖。
如果任務是通過THreadPoolExecutor.submit調用提交給線程池的,那麼這些任務在其執行過程中即便是拋出了未捕獲的異常也不會導致對其進行執行的工作線程異常終止;如果任務是通過THreadPoolExecutor.execute方法提交給線程池的,那麼這些任務在其執行過程中一旦拋出了未捕獲的異常,則對其進行執行的工作者線程就會異常終結。
通過THreadPoolExecutor調用提交給線程池執行的任務,在其執行過程中拋出的未捕獲異常並不會導致與該線程池中的工作者縣城關聯的UncaughtExceptionHandler的uncaughtExeception方法被調用。