1、概述
大家都知道线程的创建是需要消耗系统有限的资源的,如果不加限制的创建线程那么最终会拖垮整个服务,好的一点是我们可以通过线程池的方式来解决,线程池为线程生命周期开销问题和资源不足问题提供了解决方案。
2、什么是线程池
就是由多个线程组成的一个集合体,将需要执行的任务交给这个集合体,它会负责从集合体中拿出一个线程去执行任务,执行完后该线程又返回集合体等待下个任务的到来,可以看出这些线程可以被重复利用,这可以极大的减少线程的重复创建与销毁所带来的系统开销。
线程池内部主要是由工作线程与任务队列组成的,工作线程会不断循环从任务队列中取出任务然后执行,任务队列是用来存放待执行的任务。
3、线程池的创建
3.1、方式一
通过java包java.util.concurrent中的ThreadPoolExecutor来创建,其主要构造方法有:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
主要参数解释:
- corePoolSize:线程池中的核心线程个数,当每次有新任务到来时,线程池都会创建一个新的线程,直到线程池中的线程数目大于等于corePoolSize时,接下来的任务会加入任务队列,如果任务队列已满,则会创建新的线程,直到达到maximumPoolSize。
- maximumPoolSize:线程池中的最大线程个数
- keepAliveTime:线程池中除核心线程之外的其他线程的存活时间,也就是说一个非核心线程,在空闲等待新任务时的最长等待时间,如果到了时间还没有新任务可执行,就会被终止。如果该值为0,表示所有非核心线程都不会超时终止。
- workQueue:任务队列
- threadFactory:表示对创建的线程进行一些线程初始设置
- handler:表示任务的拒绝处理策略,拒绝策略需要在任务队列有界,且maximumPoolSize有限的情况下才会触发
3.2、方式二
通过java包java.util.concurrent中的Executors工厂类来创建,Executors类提供了一些用于方便创建线程池的静态工厂方法:
public static ExecutorService newCachedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newWorkStealingPool()
解释:
(1)newCachedThreadPool() 60秒超时时间的缓存线程池,内部创建线程池的的方式为:
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
注:该线程池的核心线程数为0,可创建的最大线程数无限制,线程的存活时间为60秒,任务队列使用的是 SynchronousQueue(同步阻塞队列)
优点:该线程池适用于系统负载低,且任务执行时间短的场景,因为任务无需排队,所以执行效率会更高
缺点:在高负载的场景下会导致创建出大量的线程,最终导致大量的CPU切换带来的开销,以及OOM
(2)newFixedThreadPool(int nThreads) 可指定线程数目的线程池,内部创建线程池的方式为:
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
注:该线程池的核心线程数与最大线程数都为用户指定的nThreads,创建的线程无超时时间,任务队列采用的是LinkedBlockingQueue(无界队列)
优点:创建的线程数目可控,通过无界队列可以保证新任务的排队执行
缺点:由于队列无界,如果系统负载较大,那么有可能导致队列中会积压大量任务,最终导致OOM
(3)newSingleThreadExecutor() 内部创建线程池的方式为:
return new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())
注:只有一个线程的线程池,且该线程无超时时间,任务队列采用的是LinkedBlockingQueue(无界阻塞队列)
优点:由于线程池中只有一个线程,所以队列中的任务会被顺序执行,适合需要顺序执行任务的场景
缺点:由于队列无界,如果系统负载较大,那么有可能导致队列中会积压大量任务,最终导致OOM
(4)newScheduledThreadPool(int corePoolSize) 执行定时任务的线程池,内部创建线程池的方式为:
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
注:用户可指定核心线程数corePoolSize,最大线程数无限制,线程池中的线程无超时时间,任务队列采用的是DelayedWorkQueue(延时阻塞工作队列), 只有队列中的任务的延时时间到达时,才会被线程池中的某个线程取走执行,适合需要延时执行任务的场景。
(5)newWorkStealingPool() 可以并行执行任务的线程池,jdk1.8开始支持,线程池中的线程数量为所在机器CPU的核数
并发:一般指多个线程在同一时刻同时执行一个任务,如多个用户(线程)在同一时刻同时进行网上支付操作
并行:一般指将一个大的计算密集型的任务分解成多个小任务,然后利用CPU多核的优势来分别一起执行这些小任务,最后在汇总小任务的结果(如JDK8支持对数组并行排序,或并行查找最大值,最小值),现在比较流行的Fork/Join模式就是采用的并行的编程方式
4、总结
本文主要介绍了两种创建线程池的方式,第一种创建方式更接近于底层的创建方式,相较于第二种方式稍微复杂一点,但是第一种方式更加方便我们根据不同的应用配置出合理的线程池,所以推荐使用第一种方式来创建线程池,使用第二种方式要小心,如果系统负载比较大,且任务比较耗时的情况下可能会导致OOM问题。