Java线程池
为什么要用线程池
为了减少创建和销毁线程的次数,让每个线程可以多次使用,可根据系统情况调整执行的线程数量,防止消耗过多内存,所以我们可以使用线程池。线程池的核心
生产者消费者模型:生产者将需要处理的任务放入队列;消费者从任务队列中取出任务处理。参数
corePoolSize :线程池的核心池大小,在创建线程池之后,线程池默认没有任何线程,或者说是允许线程池中允许同时运行的最大线程数
maximumPoolSize :线程池允许的最大线程数,他表示最大能创建多少个线程
keepAliveTime :表示线程没有任务时最多保持多久然后停止,只有线程池中线程数大于corePoolSize 时,keepAliveTime 才会起作用
Unit:keepAliveTime 的单位
workQueue :一个阻塞队列,用来存储等待执行的任务
threadFactory :线程工厂,用来创建线程
handler :表示当拒绝处理任务时的策略任务缓存队列
一、有界任务队列ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
二、无界任务队列LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
三、直接提交队列synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。拒绝策略
一、AbortPolicy:丢弃任务并抛出RejectedExecutionException。
二、CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。
三、DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
四、DiscardPolicy:丢弃任务,不做任何处理。线程池的关闭
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务运行
首先前面进行空指针检查,wonrkerCountOf()方法能够取得当前线程池中的线程的总数,取得当前线程数与核心池大小比较
如果小于,将通过addWorker()方法调度执行。
如果大于核心池大小,那么就提交到等待队列。
如果进入等待队列失败,则会将任务直接提交给线程池。
如果线程数达到最大线程数,那么就提交失败,执行拒绝策略。常见线程池
一、newFixedThreadPool
corePoolSize和maximumPoolSize相等,阻塞队列使用的是LinkedBlockingQueue。
当任务提交十分频繁的时候,LinkedBlockingQueue迅速增大,存在着耗尽系统资源的问题。
而且在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源,需要shutdown。
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
二、newSingleThreadExecutor
阻塞队列使用的是LinkedBlockingQueue,若有多余的任务提交到线程池中,则会被暂存到阻塞队列,待空闲时再去执行。
三、newCachedThreadPool
缓存的线程默认存活60秒。线程的核心池corePoolSize大小为0,核心池最大为Integer.MAX_VALUE,阻塞队列使用的是SynchronousQueue。
如果同时又大量任务被提交,而且任务执行的时间不是特别快,那么线程池便会新增出等量的线程池处理任务,这很可能会很快耗尽系统的资源。
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
四、newScheduledThreadPool
定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。
选择
线程池数量
一般来说CPU密集型选择固定线程池,线程数设置为cpu数量+1。
IO密集型,并发执行大量短期任务选择缓存线程池,线程数设置为cpu数量 * 2。
混合型:
NCPU = CPU的数量
UCPU = 期望对CPU的使用率 0 ≤ UCPU ≤ 1
W/C = 等待时间与计算时间的比率
如果希望处理器达到理想的使用率,那么线程池的最优大小为:
线程池大小=NCPU *UCPU(1+W/C)
线程池工厂
默认线程池工厂创建的线程都是非守护线程。
使用自定义的线程工厂可以做很多事情,比如可以跟踪线程池在何时创建了多少线程,也可以自定义线程名称和优先级。
如果将新建的线程都设置成守护线程,当主线程退出后,将会强制销毁线程池。
扩展线程池
它提供了几个可以在子类中改写的方法:beforeExecute,afterExecute和terimated。
在执行任务的线程中将调用beforeExecute和afterExecute,这些方法中还可以添加日志,计时,监视或统计收集的功能,还可以用来输出有用的调试信息,帮助系统诊断故障。
注意
一、任务独立。如何任务依赖于其他任务,那么可能产生死锁。例如某个任务等待另一个任务的返回值或执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。
二、合理配置阻塞时间过长的任务。如果任务阻塞时间过长,那么即使不出现死锁,线程池的性能也会变得很糟糕。在Java并发包里可阻塞方法都同时定义了限时方式和不限时方式。例如Thread.join,BlockingQueue.put,CountDownLatch.await等,如果任务超时,则标识任务失败,然后中止任务或者将任务放回队列以便随后执行,这样,无论任务的最终结果是否成功,这种办法都能够保证任务总能继续执行下去。
3.设置合理的线程池大小。只需要避免过大或者过小的情况即可,上文的公式线程池大小=NCPU *UCPU(1+W/C)。
4.选择合适的阻塞队列。newFixedThreadPool和newSingleThreadExecutor都使用了无界的阻塞队列,无界阻塞队列会有消耗很大的内存,如果使用了有界阻塞队列,它会规避内存占用过大的问题,但是当任务填满有界阻塞队列,新的任务该怎么办?在使用有界队列是,需要选择合适的拒绝策略,队列的大小和线程池的大小必须一起调节。对于非常大的或者无界的线程池,可以使用SynchronousQueue来避免任务排队,以直接将任务从生产者提交到工作者线程。
异常
发生异常
当某一线程发生异常时,会将当前线程分给其它任务,此时若线程池的队列中并没有任务,该线程就会出院于WAITING的状态,所以需要处理异常。
处理异常
在我们提供的Runnable的run方法中捕获任务代码可能抛出的所有异常,包括未检测异常。
使用ExecutorService.submit执行任务,利用返回的Future对象的get方法接收抛出的异常,然后进行处理。
重写ThreadPoolExecutor.afterExecute方法,处理传递到afterExecute方法中的异常。
为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常 (不推荐)
实际异常
创建线程池的正确姿势
避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。
Spring中的线程池
有空了解一下
ForkJoinPool
一、ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好。
二、ForkJoinPool 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如 quick sort 等。
三、ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。
一、ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
二、每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从队尾取出任务来执行。
三、每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式。
四、在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。
五、在既没有自己的任务,也没有可以窃取的任务时,进入休眠。
当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。
Fork/join实现Excel批量导入去重
【小家java】Java线程池之---ForkJoinPool线程池的使用以及原理
由浅入深理解Java线程池及线程池的如何使用
ThreadPoolExcutor 线程池 异常处理 (上篇)
ThreadPoolExcutor 线程池 异常处理 (下篇)
【小家java】Java中的线程池,你真的用对了吗?(教你用正确的姿势使用线程池,Executors使用中的坑)