Java 5 添加了一个新的包到Java平台,这个包是java.util.concurrent包(简称JUC)。这个包包含了有一系列能够让Java的并发编程更加轻松的类。
发现问题:
在最开始的时候,是没有线程池的概念的。每发布一个任务都要创建一个线程
/** * @Author 田海超 * @Date 2019/12/2 9:46 * @Description TODO **/ public class ThreadExample { public static void main(String[] args) { ThreadTask task0 = new ThreadTask(); ThreadTask task1 = new ThreadTask(); ThreadTask task2 = new ThreadTask(); Thread t0 = new Thread(task0); t0.start(); } static class ThreadTask implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } }
import java.util.ArrayList; import java.util.List; /** * @Author 田海超 * @Date 2019/12/2 9:46 * @Description TODO **/ public class ThreadExample { public static void main(String[] args) { // 创建任务队列 List<ThreadTask> taskList = new ArrayList<>(); ThreadTask task = null; for (int i = 0; i < 10000; i++) { task = new ThreadTask(i); taskList.add(task); } // 执行任务 for (ThreadTask toDoTask : taskList){ Thread t = new Thread(toDoTask); t.start(); } } static class ThreadTask implements Runnable { private int number ; public ThreadTask(int number) { this.number = number; } @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行任务" +number); } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } } } Thread-0执行任务0 Thread-2执行任务2 Thread-4执行任务4 Thread-8执行任务8 Thread-6执行任务6 Thread-10执行任务10 Thread-14执行任务14 Thread-12执行任务12 Thread-18执行任务18 Thread-22执行任务22 Thread-26执行任务26 ………………
如上,如果有10000个任务待执行,就要创建10000个线程。观察上面的执行结果我们会发下,线程的执行顺序并不是和我们的编码顺序相同,这是因为线程的运行的顺序取决于线程调度器,有很大的随机性。
那么问题就出现了:
我们创建线程是为了充分利用cpu资源,帮我们提高执行任务的效率。但当任务过多且线程中的任务需要一定的耗时才能够完成,便会产生很大的系统开销与资源浪费:
1、创建线程时会产生系统开销,并且每个线程还会占用一定的内存等资源
2、过多的线程也会给稳定性带来危害,因为Java 程序中的线程与操作系统中的线程是一一对应的,每个系统中,可创建线程的数量是有一个上限的,不可能无限的创建。
3、线程执行完需要被回收,大量的线程又会给垃圾回收带来压力。
4、线程调度器将执行资源在过多的线程间切换也需要时间。
于是我们便迫切的需要一个线程管理者来平衡线程与系统资源之间的关系。线程池就是一个管理线程的类。
解决问题:
线程池用一些固定的线程一直保持工作状态并反复执行任务,这样避免了反复创建线程的开下,同时控制了固定线程的数量,避免了线程数量过的来来的内存资源占用和超出操作系统线程数上线的问题。
线程池思想:首先创建了一个线程池,线程池中有 5 个线程,然后线程池将 10000 个任务分配给这 5 个线程,这 5 个线程反复领取任务并执行,直到所有任务执行完毕
import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @Author 田海超 * @Date 2019/12/2 11:08 * @Description TODO **/ public class ThreadPoolExample { public static void main(String[] args) { // 创建任务队列 List<ThreadTask> taskList = new ArrayList<>(); ThreadTask task = null; for (int i = 0; i < 10000; i++) { task = new ThreadTask(i); taskList.add(task); } // 执行任务 ExecutorService service = Executors.newFixedThreadPool(5); for (ThreadTask doTask : taskList){ service.execute(doTask); } } static class ThreadTask implements Runnable { private int number ; public ThreadTask(int number) { this.number = number; } @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行任务" +number); } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } } } pool-1-thread-1执行任务9994 pool-1-thread-1执行任务9995 pool-1-thread-1执行任务9996 pool-1-thread-1执行任务9997 pool-1-thread-1执行任务9998 pool-1-thread-1执行任务9999 pool-1-thread-2执行任务4838 pool-1-thread-4执行任务9721 pool-1-thread-3执行任务9418 pool-1-thread-5执行任务8539
使用线程池比手动创建线程主要有三点好处。
-
第一点,线程池可以解决线程生命周期的系统开销问题,同时还可以加快响应速度。因为线程池中的线程是可以复用的,我们只用少量的线程去执行大量的任务,这就大大减小了线程生命周期的开销。而且线程通常不是等接到任务后再临时创建,而是已经创建好时刻准备执行任务,这样就消除了线程创建所带来的延迟,提升了响应速度,增强了用户体验。
-
第二点,线程池可以统筹内存和 CPU 的使用,避免资源使用不当。线程池会根据配置和任务数量灵活地控制线程数量,不够的时候就创建,太多的时候就回收,避免线程过多导致内存溢出,或线程太少导致 CPU 资源浪费,达到了一个完美的平衡。
-
第三点,线程池可以统一管理资源。比如线程池可以统一管理任务队列和线程,可以统一开始或结束任务,比单个线程逐一处理任务要更方便、更易于管理,同时也有利于数据统计,比如我们可以很方便地统计出已经执行过的任务的数量。
使用:
线程池主要有 6 个参数,其中第 3 个参数由 keepAliveTime + 时间单位组成
corePoolSiza 定义的是线程的核心线程数量,就是固有线程数量,创建之后,随着线程池始终都存在的。
MaxPoolSize 最大线程数,定义的是当核心线程池不够用的时候,最多允许线程池拥有的线程数量。
workQueue 用来定义存放任务的队列
不断向线程池中追加任务,线程池的应对流程如下:
KeepAliveTime+时间单位 当线程池中的数量超出了corePoolSize且没有任务可以做,此时线程池会检测到配置的KeepAliveTime,如果超出配置时间,无事可做的临时线程就会被销毁,释放资源。
我们也可以用 setKeepAliveTime 方法动态改变 keepAliveTime 的参数值。
-
线程池希望保持较少的线程数,并且只有在负载变得很大时才增加线程。
-
线程池只有在任务队列填满时才创建多于 corePoolSize 的线程,如果使用的是无界队列(例如 LinkedBlockingQueue),那么由于队列不会满,所以线程数不会超过 corePoolSize。
-
通过设置 corePoolSize 和 maxPoolSize 为相同的值,就可以创建固定大小的线程池。
-
通过设置 maxPoolSize 为很高的值,例如 Integer.MAX_VALUE,就可以允许线程池创建任意多的线程。
ThreadFactory
第四个参数是 ThreadFactory,ThreadFactory 实际上是一个线程工厂,它的作用是生产线程以便执行任务。我们可以选择使用默认的线程工厂,创建的线程都会在同一个线程组,并拥有一样的优先级,且都不是守护线程,我们也可以选择自己定制线程工厂,以方便给线程自定义命名,不同的线程池内的线程通常会根据具体业务来定制不同的线程名。
workQueue 和 Handler
最后两个参数是 workQueue 和 Handler,它们分别对应阻塞队列和任务拒绝策略,在后面进行详细展开讲解。
新问题:那么,被线程池拒绝了的任务去了哪里?
两种情况下线程池会拒绝任务:
1、当我们调用了shutdown等方式关闭了线程池之后,即便此时可能线程池内部依然有没执行完的任务正在执行,但是由于线程池已经关闭,此时如果再向线程池内提交任务,就会遭到拒绝。
2、线程池没有能力继续处理新提交的任务,也就是上文所说的,workQueue已经满了,而且线程池中的线程数已经达到了MaxPoolSize。
解决:
第一种是主动调的关闭线程池,所以常规下码者肯定会对剩下的任务做出处理。我们现在来考虑下第二种,运行时才会知道的拒绝。
线程池对于拒绝给我们提供了四种策略,都实现了 RejectedExecutionHandler 接口:
首先,新建线程池时可以指定它的任务拒绝策略,例如:
newThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.DiscardOldestPolicy());
-
第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
-
第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
-
第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
-
第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
-
第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
-
第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。