线程的未捕获异常与监控
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方法被调用。