线程池的主要工作时控制运行线程的数量,处理过程中将任务放入队列中,然后在线程创建后启动这些任务 主要的特点是:线程复用、控制最大并发数、管理线程
降低资源消耗,通过线程复用降低线程创建和销毁造成的消耗
提高响应速度,让任务到达时,任务可以不需要等待线程创建就立即执行
提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统的资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控
随机找了个现有的线程池newFixedThreadPool,通过它进行跟踪,发现线程创建的核心是ThreadPoolExecutor
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } corePoolSize 线程池中的常驻核心线程数maximumPoolSize 线程池能够容纳同时执行的最大线程数,此值必须大于等于1keepAliveTime 多余线程的存活时间,当前线程数量超过corePoolSize时,当空闲的线程,空闲时间达到keepAliveTime值时,这个线程就会被销毁直到只剩下corePoolSize个线程位置unit keepAliveTime的单位workQueue 阻塞式的任务队列,被提交但尚未被执行的任务threadFactory 表示生成线程池中工作线程的线程工厂,用于创建线程 ,一般默认的即可defaultHandler 拒绝策略,当任务队列满了并且线程数量大于等于线程池的最大线程数时,无法继续为新任务服务,需要拒绝策略机制合理处理这个问题,jdk内置了4种拒绝策略(往下看)。1. 在创建了线程池后,等待提交过来的任务请求。 2. 当调用execute()方法添加一个请求任务时,线程池会做如下判断: 2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务; 2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列 2.3如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize, 那么还是要【创建非核心线程并且立刻运行这个任务】,你没看错, 是立即运行,插队 2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize, 那么线程池会【启动饱和拒绝策略来执行】。 3.当一个线程完成任务时,它会从队列中取下一个任务来执行。 4.当一个线程无事可做超过一-定的时间(keepAliveTime) 时,线程池会判断: 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。.
理解了线程池的参数及原理,那么就要想想怎么使用它了
JDK内置了6种线程池:
newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutornewSingleThreadScheduledExecutornewScheduledThreadPoolnewWorkStealingPool(java8新出的线程池)虽然JDK提供了现成的线程池,但是实际开发中基本上没人使用,下面介绍一下这几个线程池,你也就明白了:
newFixedThreadPool,Fixed中文为固定的,也就是说newFixedThreadPool中文意思是固定线程的线程池,通过上述知道了ThreadPoolExecutor各个参数的含义,再结合这个源码,发现它的corePoolSize与maxPoolSize,是相同的,也就是说,线程空余时间是无效的,没起作用的,这个线程池的线程永远不会被销毁。 而且使用的阻塞队列是LinkedBlockingQueue(),并且没有传参数,通过源码看到它的默认上限是Integer.Max_Value,21E个数据,队列没满是不会触发拒绝策略的,所以,如果不断的添加,内存就被撑爆了。
缓存线程池,核心线程数为0,最大线程数Integer.MAX_VALUE,队列SynchronousQueue,首先因为最大线程数是Integer.MAX_VALUE,也就是说,可以无限制的创建线程,来一个任务就处理一个任务,所以基本上不可能使用到队列,这个队列完全可以忽略了。SynchronousQueue队列的特点是每个插入操作都必须有对应的取出操作,没取出时无法继续放入,但那又能怎样呢,最大的线程数是Integer.MAX_VALUE。
单例线程,可以看到核心线程与最大线程数都是1,,也就是说始终只有1个线程在执行任务,队列为LinkedBlockingQueue,可以无限制的往队列中添加任务
定时执行的线程池,队列是延时队列,元素需要实现 Delayed 接口。线程存活时间为0,只要一空闲就被回收。
看名见义,源码也是,就是最大只有1个活动线程,并且是定时执行任务。
一个具有抢占式操作的线程池,stealing 翻译为抢断、窃取的意思,它实现的一个线程池和上面5种都不一样,用的是 ForkJoinPool 类,而不是ThreadPoolExecutor,所以在这里不会对他叙述
简单介绍了一下现成的线程池,通过各个参数也了解了它的工作原理,适不适用需要结合业务进行判断。
上面说了那么多,现成的线程池,但是在生产上,不会使用这些,因为他们提供的都是无限制大的,所以实际开发中,如果面试时你说使用的是内置的线程池,那基本上你就可以跟这个工作说拜拜了。 使用线程池时,一定要自定义!一定要自定义!一定要自定义! 以及自定义拒绝策略。
内置的拒绝策略:
1. AbortPolicy(默认):直接抛出RejectedExecutionHandler异常,并阻止系统正常运行 2. CallerRunsPolicy:“调用者运行机制”,一种调节机制,该策略不会拒绝任务,也不会抛异常,而是将某些任务回退到调用者,由调用者执行 3. DiscardOldestPolicy:抛弃队列中的等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务 4. DiscardPolicy:直接丢弃任务,不予任务处理也不抛异常。如果允许任务丢失,这是最后的一种方案。很不友好,所以实际开发中,一定要结合具体的业务,定制合适的拒绝策略。
shutdown() 方法用来关闭线程池,拒绝新任务。执行shutdown()方法后,线程池状态变为SHUTDOWN状态,此时,不能再往线程池中添加新任务,否则会抛出RejectedExecutionException异常。此时,线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出,即在终止前允许执行以前提交的任务。
还有一个类似的方法shutdownNow(),执行shutdownNow()方法后,线程池状态会立刻变成STOP状态 ,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,会返回那些未执行的任务。ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
这个需要从根据你的业务考虑
CPU密集的意思是业务需要大量的运算,而没有阻塞,CPU一直在全速运行,CPU密集任务只有再真正的多核CPU上才可能得到加速(通过多线程),CPU密集型业务配置尽可能少的的线程数量,减少线程的上下文切换,一般公式:CPU核心数+1个线程的线程池
阻塞队列的原理是,将进入队列中的线程阻塞,如果使用非阻塞队列,它不会对当前线程进行阻塞,就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦。我们反过来再从另一个角度想一下,如果线程没有阻塞而加入队列,那跟不加入队列有什么不同呢,所以,如果不阻塞而加入队列的话完全没有意义。