线程池
为什么使用线程池
多线程的软件设计方法可以最大限度地发挥多核处理器的计算能力,提升系统的性能,但是若是随意使用线程,不加以控制和管理,对系统反而会产生不利的影响,因为线程的创建和销毁需要CPU和内存资源,所以JDK为我们提供了线程池用来管理线程,免去了不必要的开支。
线程池的使用能够为我们带来如下好处
- 降低资源消耗:如果我们每次执行异步任务都去new一个线程来运行,那么线程的创建和销毁的开销则是难于预估的,而使用线程池就可以直接对线程复用来降低资源消耗。
- 提高线程的可管理性:使用线程池可以进行统一分配、调优和监控。
JDK已经为我们提供好了一套Executor框架,帮助开发人员来有效地进行线程控制。
首先我们来看一下涉及到的类图,这些类和接口均在JUC包中。
基础的接口
从Executor开始
Executor是JDK1.5时,随着J.U.C引入的一个接口,引入该接口的主要目的是解耦任务和执行。我们之前通过线程执行一个任务时,往往需要先创建一个线程,然后调用线程的start方法来执行任务,本身既是任务又得做执行操作。
new Thread(new RunnableTask()).start();
而Executor接口解耦了任务和执行机制,任务为Runnable和Callable等,而执行则交给Executor。该接口只有一个方法,入参为待执行的任务:
public interface Executor {
//执行给定的任务
//根据Executor的实现不同, 具体执行方式也不相同.
void execute(Runnable command);
}
我们可以像下面一样执行任务,而不必关心线程的创建。
Executor executor = someExecutor; // 创建具体的Executor对象
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
我们知道JAVA的线程和本地操作系统的线程是一对一对应的,JAVA线程启动时会创建一个操作系统线程,当该JAVA线程终止时,操作系统线程同样会被回收。操作系统会调度所有的线程分配给可用的CPU,由于CPU资源是有限的,线程数量是有上限的,所以一般使用用户的Executor来固定线程的数量和管理,上层通过Executor控制调度,下层由操作系统控制,这其实也是线程池的雏形。
增强的Executor—ExecutorService
ExecutorService继承了Executor,它在Executor的基础上增强了对任务的控制,同时包括对自身生命周期的管理。
public interface ExecutorService extends Executor {
//关闭执行器
//(1)已经提交给该执行器的任务将会继续执行, 但是不再接受新任务的提交;
//(2)如果执行器已经关闭了, 则再次调用没有作用
//(3)不等待以前提交的任务完成执行
void shutdown();
//立即关闭执行器
//(1)尝试停止所有正在执行的任务,并返回等待执行的任务列表。
//(2)停止处理已提交但未执行的任务
//(3)无法保证能够停止成功, 但会尽力尝试(例如, 通过 Thread.interrupt中断任务, 但是不响应中断的任务可能无法终止)
List<Runnable> shutdownNow();
//如果该执行器已经关闭, 则返回true.
boolean isShutdown();
//如果关闭后所有任务都已完成则返回true
//除非首先调用 shutdown 或 shutdownNow, 否则该方法永远返回false.
boolean isTerminated();
//阻塞调用线程
//直到所有任务在关闭后完成执行,或发生超时,或当前线程中断等返回true
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
//提交一个具有返回值的任务用于执行.
//通过Future的get方法在成功完成时获取task的返回值.
//task:提交的任务 T:返回值的类型
<T> Future<T> submit(Callable<T> task);
/**
* 提交一个 Runnable 任务用于执行.
* 注意: Future的get方法在成功完成时将会返回给定的结果(入参时指定).
*
* @param task 待提交的任务
* @param result 返回的结果
*/
<T> Future<T> submit(Runnable task, T result);
/**
* 提交一个 Runnable 任务用于执行.
* 注意: Future的get方法在成功完成时将会返回null.
*/
Future<?> submit(Runnable task);
//执行给定集合中的所有任务, 当所有任务都执行完成后, 返回保留任务状态和结果的 Future 列表.
//如果等待时发生中断, 会将所有未完成的任务取消.
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
//执行给定集合中的所有任务, 当所有任务都执行完成后或超时期满时(无论哪个首先发生)
//返回保留任务状态和结果的 Future 列表.
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
//执行给定集合中的任务, 只要其中某个任务率先成功完成(未抛出异常), 则返回其结果.
//一旦正常或异常返回后, 则取消尚未完成的任务.
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
//执行给定集合中的任务, 如果在给定的超时期满前, 某个任务已成功完成(未抛出异常), 则返回其结果.
//一旦正常或异常返回后, 则取消尚未完成的任务.
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
周期任务的调度—ScheduledExecutorService
在一些情况下,我们可能希望提交给执行器的某些任务可以定时执行,所以在ExecutorService的基础上,又提供了一个接口——ScheduledExecutorService,ScheduledExecutorService提供了一系列schedule方法,可以在给定的延迟后执行提交的任务,或者每个指定的周期执行一次提交的任务。
public interface ScheduledExecutorService extends ExecutorService {
/**
* 提交一个待执行的任务, 并在给定的延迟后执行该任务.
*
* @param command 待执行的任务
* @param delay 延迟时间
* @param unit 延迟时间的单位
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
/**
* 提交一个待执行的任务(具有返回值), 并在给定的延迟后执行该任务.
*
* @param command 待执行的任务
* @param delay 延迟时间
* @param unit 延迟时间的单位
* @param <V> 返回值类型
*/
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
/**
* 创建并执行一个周期性任务。
* 该任务首先在 initialDelay 后开始执行, 然后在 initialDelay+period 后执行, 接着在 initialDelay + 2 * period 后执行, 依此类推.
* 如果任务的任何执行遇到异常,则禁止后续执行。
* 否则,任务将仅通过取消或终止执行者而终止。
* 如果此任务的任何执行时间超过其周期,则后续执行可能延迟开始,但不会并发执行。
*
* @param command 待执行的任务
* @param initialDelay 首次执行的延迟时间
* @param period 连续执行之间的周期
* @param unit 延迟时间的单位
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
/**
* 创建并执行一个周期性任务,该任务在给定的初始延迟之后首先启用。
* 然后在一次执行结束和下一次执行开始之间具有给定的延迟。
* 如果任务的任何执行遇到异常,则禁止后续执行。
* 否则,任务将仅通过取消或终止执行者而终止。
*
*
* @param command 待执行的任务
* @param initialDelay 首次执行的延迟时间
* @param delay 一次执行终止和下一次执行开始之间的延迟
* @param unit 延迟时间的单位
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
以上就是Executors框架中的三个最核心的接口。
线程池工厂类Executors
Executors
上面三种执行器只是接口,J.U.C提供了许多默认的接口实现。但是大部分情况下不需要我们去创建这些类的实例,因为J.U.C中还提供了一个Executors类,专门用于创建上述接口的实现类对象。Executors其实就是一个简单工厂,它的所有方法都是static的,用户可以根据需要,选择需要创建的线程池实例,Executors一共提供了五类可供创建的Executor执行器实例。
固定线程数的线程池
Executors提供了两种创建具有固定线程数的Executor的方法,固定线程池在初始化时确定其中的线程总数,运行过程中会始终维持线程数量不变。当有新任务提交时,如果线程池中有空闲线程则立即执行,若没有,则任务会被存放在一个任务队列中,待有线程空闲时,便处理任务队列中的任务。
两种创建方法其实都返回了一个ThreadPoolExecutor实例。ThreadPoolExecutor是一个ExecutorService接口的实现类,我们会在后面讲解。
/**
* 创建一个固定线程数量的线程池
* 如果所有线程都处于活动状态时提交了其他任务,则将在队列中等待线程可用。
* 如果线程在执行过程中由于失败而终止,则在执行后续任务时将使用新线程来代替它。
*
* nThreads:创建的线程数量
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
/**
* 创建一个具有固定线程数的线程池
* 在需要时使用提供的 ThreadFactory 创建新线程.
* 如果所有线程都处于活动状态时提交了其他任务,则将在队列中等待线程可用。
* 如果线程在执行过程中由于失败而终止,则在执行后续任务时将使用新线程来代替它。
*
* @param nThreads 线程数量
* @param threadFactory 创建线程池的工厂
*/
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
第二个方法在创建线程池时需要传递一个创建线程的工厂类,为什么这样做呢?因为线程池的创建肯定需要创建线程,一般情况下我们需要通过new Thread()这种方式去创建线程,但是我们可能希望给线程设置一些属性,例如名称、守护线程、线程组等等。线程池中的线程非常多,如果每一个都手动配置的话就非常繁琐,而ThreadFactory作为线程工厂就可以很好的帮我们解决这个问题,我们只需要指定ThreadFactory实例,就可以决定线程的具体创建方式。
Executors提供了静态内部类,实现了ThreadFactory接口,最简单且常用的就是下面这个DefaultThreadFactory
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
可以看到,DefaultThreadFactory 初始化的时候定义了线程组、线程名称等信息,每创建一个线程,都给线程统一分配这些信息,避免了一个个手工通过new的方式创建线程,又可进行工厂的复用。
我们来看一个简单的使用案例。
public class FixThreadPoolDemo {
static class MyTask implements Runnable{
@Override
public void run() {
System.out.println(System.currentTimeMillis()+":线程id:"+Thread.currentThread().getId());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyTask task=new MyTask();
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i=0;i<10;i++){
executorService.submit(task);
}
}
}
我们创建了一个只有5个线程的线程池,然后提交了10个任务,所以会分两批次执行,前后相差10000毫秒。
单个线程的线程池
Executors还提供了两种创建只有单个线程的线程池的方法。该方法返回一个只有一个线程的线程池,若有多余的任务提交则会被放入任务队列,待线程空闲,按照先进先出的规则依次执行。
/**
*创建一个使用单个worker线程的Executor
*/
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
/**
* 创建一个使用单个worker线程的Executor
* 在需要时使用提供的 ThreadFactory 创建新线程.
*/
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
可以看到,只有单个线程的线程池其实就是指定线程数为1的固定线程池,主要区别就是,返回的Executor实例用了一个FinalizableDelegatedExecutorService对象进行包装。
我们来看下FinalizableDelegatedExecutorService,该类 只定义了一个finalize方法:
static class FinalizableDelegatedExecutorService
extends DelegatedExecutorService {
FinalizableDelegatedExecutorService(ExecutorService executor) {
super(executor);
}
protected void finalize() {
super.shutdown();
}
}
核心是其继承的DelegatedExecutorService ,这是一个包装类,实现了ExecutorService的所有方法,但是内部实现其实都委托给了传入的ExecutorService 实例:
static class DelegatedExecutorService extends AbstractExecutorService {
private final ExecutorService e;
DelegatedExecutorService(ExecutorService executor) { e = executor; }
public void execute(Runnable command) { e.execute(command); }
public void shutdown() { e.shutdown(); }
public List<Runnable> shutdownNow() { return e.shutdownNow(); }
public boolean isShutdown() { return e.isShutdown(); }
public boolean isTerminated() { return e.isTerminated(); }
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
return e.awaitTermination(timeout, unit);
}
public Future<?> submit(Runnable task) {
return e.submit(task);
}
public <T> Future<T> submit(Callable<T> task) {
return e.submit(task);
}
public <T> Future<T> submit(Runnable task, T result) {
return e.submit(task, result);
}
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
return e.invokeAll(tasks);
}
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
return e.invokeAll(tasks, timeout, unit);
}
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
return e.invokeAny(tasks);
}
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return e.invokeAny(tasks, timeout, unit);
}
}
为什么要多此一举,加上这样一个委托层?因为返回的ThreadPoolExecutor包含一些设置线程池大小的方法——比如setCorePoolSize,对于只有单个线程的线程池来说,我们是不希望用户通过强转的方式使用这些方法的,所以需要一个包装类,只暴露ExecutorService本身的方法。
可缓存的线程池
有些情况下,我们虽然创建了具有一定线程数的线程池,但出于资源利用率的考虑,可能希望在特定的时候对线程进行回收(比如线程超过指定时间没有被使用),Executors就提供了这种类型的线程池。可以创建一个根据实际情况调整线程数量的线程池。线程池的线程数量不固定,若所有线程都在工作,又有新的任务提交,则会创建新的线程。
/**
* 创建一个线程池,该线程池根据需要创建新线程
* 如果线程池中没有现有线程可用,则将创建新线程并将其添加到池中。
* 默认情况下60秒未使用的线程将被终止并从缓存中移除。
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
/**
* 创建一个线程池,该线程池根据需要创建新线程
* 如果线程池中没有现有线程可用,则将创建新线程并将其添加到池中。
* 默认情况下60秒未使用的线程将被终止并从缓存中移除。
* 在需要时使用提供的 ThreadFactory 创建新线程.
*/
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
可以看到,返回的还是ThreadPoolExecutor对象,只是指定了超时时间,另外线程池中线程的数量在[0, Integer.MAX_VALUE]之间。
可延时/周期调度的线程池
如果有任务需要延迟/周期调用,就需要返回ScheduledExecutorService接口的实例,ScheduledThreadPoolExecutor就是实现了ScheduledExecutorService接口的一种Executor,和ThreadPoolExecutor一样,这个我们后面会专门讲解。
/**
* 创建一个具有固定线程数的可调度的线程池.
* 该线程池可以计划在给定的延迟,或周期性地执行。
*/
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
/**
* 创建一个具有固定线程数的可调度的线程池.
* 该线程池可以计划在给定的延迟,或周期性地执行。
* 在需要时使用提供的 ThreadFactory 创建新线程.
*/
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
我们来看一个简单的使用案例。
public class SchedulePoolDemo {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
},0,2, TimeUnit.SECONDS);
}
}
我们创建了一个周期性的线程池,每两秒执行一次任务。
如果我们的任务执行时间超过2s,那么它的任务会重叠嘛?我们可以测试一下:
public class SchedulePoolDemo {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis());
}
},0,2, TimeUnit.SECONDS);
}
}
我们发现任务是不会重叠的,ScheduledExecutorService 会让任务结束之后立马执行下一次任务,所以周期就变成了5秒。
基础的知识已经了解完毕,下一章我们会详细讲解ThreadPoolExecutor和ScheduledThreadPoolExecutor。