在一些需要使用线程去处理任务的业务场景中,如果每一个任务都创建一个线程去处理,任务处理完过后,把这个线程销毁,这样会产生大量的线程创建,销毁的资源开销。使用线程池能有效的控制这种线程的创建和销毁,而且能够对创建的线程进行有效的管理。
主要是用来执行提交的任务。下面是接口定义:
public interface Executor {
void execute(Runnable command);
}
后面说的线程池会实现这个接口,并且会使用这个方法来提交一个任务。
ExecutorService接口是Executor接口的一个子接口,它在Executor接口的基础上增加了一些方法,用来支持对任务的终止管理以及对异步任务的支持。
public interface ExecutorService extends Executor {
void shutdown();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
Future submit(Callable task);
Future submit(Runnable task, T result);
Future> submit(Runnable task);
List> invokeAll(Collection extends Callable> tasks)
throws InterruptedException;
List> invokeAll(Collection extends Callable> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
T invokeAny(Collection extends Callable> tasks)
throws InterruptedException, ExecutionException;
T invokeAny(Collection extends Callable> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
AbstractExecutorService实现了ExecutorService,并基于模板方法模式对一些方法给出了实现。是我们接下来要提到的线程池类ThreadPoolExecutor的直接父类。代码贴出来有点多,这里就不贴了。
ThreadPoolExecutor通常就是我们所说的线程池类,Java的线程池就是用过这个类进行创建的。下面分析的线程池的运行原理,也是基于这个类来进行分析的。
ScheduledExecutorService接口是ExecutorService子接口,定义了线程池基于任务调度的一些方法。
public interface ScheduledExecutorService extends ExecutorService {
public ScheduledFuture> schedule(Runnable command,
long delay, TimeUnit unit);
public ScheduledFuture schedule(Callable callable,
long delay, TimeUnit unit);
public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
可以看到,上面定义了延时周期调度,固定频率周期调度,返回任务结果的任务调度等方法。
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,并且实现了ScheduledExecutorService接口,对任务调度的功能进行了实现。
Executors可以认为是线程池的工厂类,里面提供了静态方法对线程池进行创建。
下面列出常用的几种线程池创建方法:
//固定线程大小的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
//单个线程线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
//无上限线程线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
//基于任务调度的线程池(还有其他类型的任务调度线程池,这里不列举了)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
以上就是Java线程池相关的API类。
这里主要介绍Java线程池各种情况下,一个任务提交给线程池,它在线程池内部是怎么运行的。在介绍内部运行机制之前,有必要先对线程池的一些参数属性进行介绍。
上面介绍了线程池内部的一些核心属性,下面会基于这些属性来介绍,当用户提交一个任务时,线程池内部是如何运行的。
这里主要介绍使用Executors类来创建的几种线程池,及其比较适合的使用场景。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
可以看到,线程池里最多一直有个线程用来处理任务,并且队列使用的是无界队列LinkedBlockingQueue。这种线程池主要适用于请求量非常小的场景,或者离线的数据处理等,只需要一个线程就够了。在持续的请求量比较大的情况下,不要使用这种线程池,单线程处理会使队列不断变大,最终可能导致oom(内存溢出)。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
可以看到corePoolSize和maximumPoolSize是相等的,keepAliveTime设置为0,队列用的是LinkedBlockingQueue无界队列。这种线程池适用于流量比较稳定的情况,不会说一段时间突然有大量的流量涌入,导致LinkedBlockingQueue越来越大最后导致内存溢出。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
可以看到corePoolSize=0,maximumPoolSize是Integer.MAX_VALUE, keepAliveTime为60秒,队列使用的是SynchronousQueue同步队列,这个队列可以理解为没有容量的阻塞队列,只有有别的线程来拿任务时,当前线程任务才能插入成功,反过来也是一样。所以这种线程池任务队列是不存任务的,任务全靠创建新的线程来处理,处理完了过后线程空闲超过60秒就会被销毁。所以这种线程池适合有一定高峰流量的场景。但是还是要慎用,如果流量过高,会导致创建的线程过多,直接导致服务所在机器的CPU负载过高,然后机器卡死。所以如果使用这种线程池一定要流量是开发者知道的,最高峰时候的流量也不会导致CPU负载过高,才能使用这种线程池。
可以根据自己的需求,使用单线程调度(SingleThreadScheduledExecutor),多线程调度(ScheduledThreadPool)。不过现在使用spring调度比较多点,我自己在开发中使用线程池的调度也比较少了,基本会使用spring的调度。
个人比较推荐这种方式,根据实际的一个业务场景,自己new一个ThreadPoolExecutor,参数根据业务场景需要指定合适的参数,比如核心线程数设置多少合适,最大线程数设置多少合适,任务队列设置多大的有界合适,拒绝策略也可以自定义,一般采用离线存储啥的,完全根绝自己的业务场景来定制。这样可以保证不会发生无界队列导致oom,也不会导致创建的线程过多而导致机器卡死(一般I5、4核的处理器跑1000左右的线程就会负载过高)。
这些问题开发者可以使用上面提高的自定义创建线程池自行根据业务场景来设置线程池的参数,从而规避上述的一些问题。
这种情况,如果业务场景是需要保证消息的百分百不丢失,那就需要在提交任务时,对任务做离线存储,在任务执行完过后,再将对应的离线存储的任务删除。服务启动后,需要起一个后台线程去加载一次离线存储的任务,提交给线程池去执行。