深入分析JAVA线程池

一. 线程池的作用


1.实现了线程复用,减少了线程创建和撤销带来的系统消耗。


2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。


3.方便管理,控制了系统中线程的数量,避免因系统无休止的创建线程而导致系统崩溃。


二. 线程池的适用场合


       任务运行时间短并且执行频率较高的场合比较适合使用线程池。(最近帮导师做的放大器定位系统中用户的一次放大器定位检测需要一个线程来完成;若用户提交一个文件实现一组放大器定位检测时需要使用线程池)。运行时间较长并且执行频率不高的场合不适合使用线程池。


三. 线程池可能带来的问题


1.使用线程池不当可能会导致死锁。之前在测试乐观锁性能时遇到过这个问题,主要是利用循环栅栏让多个线程同时启动时因使用线程池不当导致了死锁。导致死锁的原因是:提交给线程池的任务不是相互独立的,所有的任务提交到线程池后首先阻塞当前线程,当最后一个任务提交后,该任务首先重新唤醒所有之前阻塞的线程,确保所有线程同时启动。但由于线程池中的可用线程已经被前面的任务占用,导致最后一个任务提交后没有线程可用而放入阻塞队列中,无法唤醒其它阻塞的线程,出现死锁。


2.当任务出现异常时,不易察觉。特别是当调用submit函数时,后面会有详细的说明。


3.使用线程池会有部分的线程常驻内存,如果使用Thread Local,如果用户不主动释放的话,只有当Thread被虚拟机撤销后,其对应的Thread Local才会被虚拟机回收,因此当Thread Local引用了重对象时可能会出现OOM。


4.提交给线程池的任务如果不是可中断的,可能导致该任务永远无法停止。主要的原因是:线程池是通过中断线程的方式来关闭任务执行的。


四. Java线程池分类


       固定大小的线程池 :newFixedThreadPool

       线程池大小可变的线程池:newCachedThreadPool

       执行定时任务的线程池:newScheduledThreadPool


五. Java线程池中相关的参数详解

        

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)


    上述是创建线程池的构造函数,构造函数中的每一个参数的含义如下:

     

      corePoolSize:线程池中基本线程数量。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于corePoolSize时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。


       maximumPoolSize:线程池中的最大线程数量。如果队列满了,并且已创建的线程的数量小于最大线程数,则线程池会再创建新的线程执行任务。需要注意的是如果使用了无界的任务队列这个参数就没什么效果。


       keepAliveTime:线程活动保持时间。当线程池中线程数量超过corePoolSize时,多余的空闲线程的存活时间。即,超过corePoolSize的空闲线程,在多长时间内,会被撤销。如果任务很多,并且每个任务执行的时间比较短,可以适当调大这个时间,提高线程的利用率。


    unit:keepAliveTime的单位。可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。


      workQueue:任务队列,保存被提交但尚未被执行的任务。

           有界的任务队列:ArrayBlockingQueue队列。

           无界的任务队列:LinkedBlockingQueue队列。

           直接提交队列:SynchronousQueue队列。

           优先任务队列:PriorityBlockingQueue队列。


      ThreadFactory:用来创建线程的工厂。通过线程工厂来每一个创建出来的线程设置更有效的名字。


        RejectedExecutionHandler:拒绝策略。当线程池和队列都满了后,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。

        AbortPolicy:直接抛出异常。

       CallerRunsPolicy:用调用者所在线程来运行任务。

       DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

      DiscardPolicy:不处理,丢弃掉。


     当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。


六. 向线程池提交任务


       向线程池提交任务有两种方式,一种是execute(Runnable r),另一种是submit方法。可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务是否被线程池执行成功。也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。


七. 线程池执行任务时的异常


      如果使用execute(Runnabler)来提交任务,runnable没有经过多余的封装,runWorker中得到的异常可以在afterExecute中得到处理,如果用户没有扩展线程池以重新实现afterExecute方法,那么对于线程运行时抛出的异常,jvm会自动调用thread中的dispatchUncaughtException来处理。这时可以在控制台看到出错的堆栈信息。


       如果使用submit来提交任务,runnable首先会被封装成FutureTask。FutureTask会重新实现run方法,会捕获runnable中run方法执行过程中抛出的所有异常,并利用setException方法将异常封装到了futher中。因此线程池中的runWorker方法中是捕获不到任何异常的,传入到afterExecute方法中的throwable为null,因此用户通过重新实现afterExecute方法来扩展线程池时也需要通过Future接口调用get方法去取结果,才能拿到异常。


八. 如何解决线程池中未捕获异常信息丢失问题


       在使用 JUC 的线程池时(ThreadPoolExecutor 等),需要注意执行任务时可能会抛出的各种 RuntimeException。如果直接使用 JDK 提供的线程池实现的时候,很有可能发生任务“悄无声息”、“莫名其妙”地就结束了。其实,这种情况的发生是因为你没捕获并处理 Runnable 或 Callable 中发生的异常。


        在你写命令行程序的时候,上述情况并不会发生(使用execute提交任务,使用submit提交任务,如果不用get获取结果也会发生。)。那是因为线程的默认 UncaughtExceptionHandler 会将异常栈信息输出到命令行界面上,所以大家都知道任务因为异常的发生而退出。但是,在服务器应用中,没有人会一直去看控制台输出。这时,如果还是使用默认的 UncaughtExceptionHandler 就不合适了。


       比较好的方法是将异常信息记录到日记中,同时可能需要释放相关的资源。


      解决的方案具体如下:

      1.在提交的任务中将异常捕获并处理,不抛给线程池。


      2.异常抛给线程池,但我们需要及时处理抛出的异常信息。

 

       对于方法1,思路很简单,但是这种思路的缺点就是:1)所有的不同任务类型都要trycatch,增加了代码量。2)不存在checkedexception的地方也需要都trycatch起来,代码丑陋。所以方法1基本不会使用。


       对于方法2,主要包括如下的一些思路:


      自定义线程池:实现模板方法afterExecute(Runnable r,Throwable t)。在afterExecute方法处理异常,可以将异常写入日记。


       给线程池中的线程提供UncaughtExceptionHandler,在uncaughtException(Thread t,Throwable e)处理异常。可以自定义通过给线程池中创建线程的ThreadFactory。并在newThread方法中给创建的线程设置自定义的UncaughtExceptionHandler。


        采用Future模式,将返回结果以及异常放到Future中,在Future中处理。如果提交任务的时候使用的方法是submit,那么该方法将返回一个Future对象,所有的异常以及处理结果都可以通过future对象获取。

 

        需要注意的是,对于submit提交的任务,异常放在future中,如果采用自定义线程池来处理异常,afterExecute方法中参数Throwable t为null,因此需要调用future的get方法才会捕获到异常。


      扩展线程池


       扩展线程池主要包括:重新实现线程池提供的三个模板方法或者设置自定义相关参数。


      befoerExecute(Thread t,Runnabler)  在任务执行之前做一些操作,比如记录任务的开始时间(用到了ThreadLocal)。


      afterExecute(Runnable r,Throwablet)  在任务结束之后做一些操作,比如计算任务执行的时间,或者处理任务在执行期间出现的异常。


      terminated() 在退出线程池后退出后做一些操作,比如记录完成任务的个数,执行任务平均花费时间。


     提供自定义拒绝策略。通过setRejectedExecutionHandler方法提供自定义拒绝策略,比如将相关记录记入到日记中。


      提供自定义线程工厂。通过setThreadFactory方法提供自定义线程工厂,比如设置线程的name为可识别的,方便调试。或者为了方便处理异常,可以设置线程的UncaughtExceptionHandler。


       提供自定义的缓冲队列。默认的线程池ThreadPoolExecutor当活动线程数量大于corePoolSize时,先将任务放入的缓冲队列中,对于紧急任务,这样是不太明智的,特别当队列是无界队列的时候。因此此时需要带有优先级的阻塞队列或者能够根据线程池中线程的数量动态调整执行流程的阻塞队列。


     下面是我通过重新实现线程池提供的三个模板方法来扩展线程池,扩展后的线程池可以记录各个任务执行的时间以及所有任务执行完成后的总时间,详细代码如下:



import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;

/**
 * 
 * @author wfzhou 扩展线程池,监控任务的执行时间
 *
 */
public class TimeThreadPoolExecutor extends ThreadPoolExecutor {
	private final Logger log = Logger.getAnonymousLogger();
	// 存储任务执行的开始时间,这里用到了threadLocal
	private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
	// 记录完成的任务数量,因为多线程会同时修改,因此这里使用AtomicLong
	private AtomicLong taskCounts = new AtomicLong();
	// 记录任务所有任务执行完所花费的时间
	private AtomicLong sumTimes = new AtomicLong();

	@Override
	protected void beforeExecute(Thread t, Runnable r) {
		log.info("Thread " + t.getName() + " start execute  " + r.toString());
		startTime.set(System.nanoTime());
	}

	@Override
	protected void afterExecute(Runnable r, Throwable t) {
		if (t != null) {// 执行任务出现异常
			log.info("execute  " + r.toString() + " throw excpetion: "
					+ t.getMessage());
		} else {
			Long endTime = System.nanoTime();
			Long executeTime = endTime - startTime.get();
			taskCounts.incrementAndGet();
			sumTimes.addAndGet(executeTime);
			log.info("execute  " + r.toString() + " spend time:" + executeTime
					+ "ns");
		}
	}

	@Override
	protected void terminated() {
		log.info("execute  " + taskCounts.get() + " tasks spend time:"+sumTimes.get()+ " avgTime:"
				+ (sumTimes.get() / (taskCounts.get() )) + "ns");
	}

	public TimeThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
			long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
		super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
	}

}


 十. 参考文献


1. jdk1.8源码


2.Java线程池的使用    https://www.jianshu.com/p/c51298569a71


















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值