学习JDK中线程池的实现,大家最好先学习一下自定义线程池,这样可以更好理解JDK中线程池的工作原理。我们这篇文章也是基于理解大概了解线程池原理。
自定义线程池
- JDK中线程池接口和实现类
ThreadPoolExecutor
-
线程池的状态
ThreadPoolExecutor使用int的高三位来表示线程池的状态,低29位表示线程的数量。
这里有一个问题,为何不用两个数字,一个代表状态,一个代表线程数量?
这是因为这里为了线程安全,我们只需要使用一次CAS原子操作,定义一个原子整数,就可以满足需求。 -
构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数
corePoolSize - (核心线程数)即使空闲时仍保留在池中的线程数,除非设置 allowCoreThreadTimeOut 。
maximumPoolSize - 池中允许的最大线程数 (最大=核心线程+应急线程)。
keepAliveTime - 当线程数大于内核时,这是多余的空闲线程在终止前等待新任务的最大时间。(针对救急线程)。
unit - keepAliveTime参数的时间单位 (时间单位,针对救急线程)。
workQueue - (阻塞队列)用于在执行任务之前使用的队列。 这个队列将仅保存execute方法提交的Runnable任务。
threadFactory - (线程工厂,为线程起规范的名字)执行程序创建新线程时使用的工厂 。
handler -(拒绝策略) 执行被阻止时使用的处理程序,因为达到线程限制和阻塞队列容量。
图中我们看到几个实体,分别是(核心线程、阻塞队列、救急线程)。
- 这里当我们的任务来了我们的线程池中核心线程还有空闲则核心线程进行任务处理。
- 如果核心线程忙,那么任务放入阻塞队列中等待。
- 如果阻塞队列有限,阻塞队列满了,有任务到来,那么就救急线程就行处理。
- 救急线程:救急线程是总线程减去核心线程,当阻塞队列满了来的任务放入救急线程。(线程池中救急线程当用完后会结束,核心线程会一直运行)
- 拒绝策略:当最后救急线程也满了,那么就来的任务执行拒绝策略(我的博客自定义线程池中说了拒绝策略的概念)
下图是JDK中拒绝策略的类图
提交任务
-
void execute(Runnable command)
这个我们看到参数是Runnable类型的所以没有返回值。 -
Future submit(Callable task)
这里我们看到参数是Callable类型的,我们知道Callable和Runnable的区别就是返回处理结果。
通过Future,这个是保护性暂停方式去获取返回值。
保护性暂停
提交值返回任务以执行,并返回代表任务待处理结果的Future。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool= Executors.newFixedThreadPool(2);
Future<String> submit = pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "hello";
}
});
System.out.println(submit.get());
Future<String> submit1 = pool.submit(() -> {//lambda方式
return "world";
});
System.out.println(submit1.get());
}
- List<Future> invokeAll(Collection<? extends Callable> tasks)
这个我们看到传入参数是Callable的集合对象,返回的也是集合,它是给每个线程传入一组任务。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
List<Future<String>> futures = pool.invokeAll(Arrays.asList(
() -> {
return "1号";
},
() -> {
return "2号";
}
));
futures.forEach(f ->{
try {
System.out.println(f.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
}
- T invokeAny(Collection<? extends Callable> tasks)
执行给定的任务,返回一个成功完成的结果(即没有抛出异常),如果有的话。
我们看到返回的是一个Object返回的是第一个完成的任务,其他没有完成的任务取消。
关闭线程池
用来关闭线程池
- void shutdown()
启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务 - List shutdownNow()
尝试停止所有主动执行的任务,停止等待任务的处理,并返回正在等待执行的任务列表。
Executors
- 由于通过ThreadPoolExecutor构造方法创建各种类型线程池,因为构造方法参数太多不好记忆,JDK为我们提供一个工厂类,来创建我们想要的线程池。
- Executors
public class Executors
extends Object工厂和工具方法Executor , ExecutorService , ScheduledExecutorService , ThreadFactory和Callable在此包中定义的类。 该类支持以下几种方法:
创建并返回一个ExecutorService设置的常用的配置设置的方法。
创建并返回一个ScheduledExecutorService的方法, 其中设置了常用的配置设置。
创建并返回“包装”ExecutorService的方法,通过使实现特定的方法无法访问来禁用重新配置。
创建并返回将新创建的线程设置为已知状态的ThreadFactory的方法。
创建并返回一个方法Callable出的其他闭包形式,这样他们就可以在需要的执行方法使用Callable 。
- 创建固定大小线程池,阻塞队列无限。newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads)
创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。 在任何时候,最多nThreads线程将处于主动处理任务。 如果所有线程处于活动状态时都会提交其他任务,则它们将等待队列中直到线程可用。 如果任何线程由于在关闭之前的执行期间发生故障而终止,则如果需要执行后续任务,则新线程将占用它。 池中的线程将存在,直到它明确地为shutdown 。(都是核心线程,没有应急线程)
下面是方法的源码,从源码可以看出没有总线程=核心线程没有应急线程
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
使用举例
public class Demo1 {
public static void main(String[] args) {
ExecutorService pool= Executors.newFixedThreadPool(2);
pool.execute(()->{
System.out.println(Thread.currentThread().getName());
});
pool.execute(()->{
System.out.println(Thread.currentThread().getName());
});
pool.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}
这里我们的线程池和线程的名字,都是通过线程工厂来创建的,当然我们也可以自己定义自己线程池的名字。
当我们继续ThreadPoolExecutor源码,调用的是 Executors.defaultThreadFactory()来确定线程工厂那个构造方法。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
当我们想自己给我们的线程池起名字,就用两个参数的哪个重载方法。
ExecutorService pool= Executors.newFixedThreadPool(2, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r,"mythread1");
}
});
这个方法适合线程任务数量不多情况
- newCachedThreadPool() 创建没有核心线程,都是救急线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。 这些池通常会提高执行许多短暂异步任务的程序的性能。 调用execute将重用以前构造的线程(如果可用)。 如果没有可用的线程,将创建一个新的线程并将其添加到该池中。 未使用六十秒的线程将被终止并从缓存中删除。 因此,长时间保持闲置的池将不会消耗任何资源。 请注意,可以使用ThreadPoolExecutor构造函数创建具有相似属性但不同详细信息的池(例如,超时参数)。
这个适合线程数比较大,但是每个任务执行的时间较短。
- newSingleThreadExecutor() 创建一个线程池中只有一个线程
从名字可以看出这个创建时一个线程池中只有一个核心线程,没有救急线程,阻塞队列无界的线程池。
适合希望多个任务排队执行。
newSingleThreadExecutor()的源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
这里我们需要注意几个和前面的区别:
- 和前面创建固定大小的线程将参数设置为1的区别(Executors.newFixedThreadPool(1);)
这里区别要从newSingleThreadExecutor()的源码看出,我们源码中跟ewFixedThreadPool放回不一样,它是用new FinalizableDelegatedExecutorService将ThreadPoolExecutor进行包装放回,这样对外只暴露了ExecutorService的接口方法,而不能通过强转的方法,调用ThreadPoolExecutor中的特有方法,这里运用的是装饰器模式。 - 这个和自己直接创建一个线程的区别:
如果自己创建一个新线程,任务失败那就线程就会结束。但是我们用单线程线程池,当任务失败那么线程池就会立马创建一个新线程顶替线程池中失败线程。
创建多个线程的线程池合适
创建线程池中线程数过小,那么会导致饥饿问题。
多大那么会导致线程分配时间片导致上下文切换,占用更多的内存,效率过低。
-
CPU密集型运算
适合应用程序做大量计算,需要大量使用到CPU
通常采用CPU核数+1作为线程池中的线程池。加一是因为当线程由于页缺失导致故障导致或其他故障导致暂停,这个线程能立马顶上去。 -
I/O密集型
WEB应用型,CPU并不总处于繁忙状态,
线程数=核数X期望CPU利用率 X 总时间(CPU计算时间+等待时间)/ CPU计算时间
ScheduledExecutorService
- 延时执行
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.schedule(()->{
System.out.println("你好");
},1, TimeUnit.SECONDS);
}
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
创建并执行在给定延迟后启用的单次操作。
-
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
创建并执行在给定的初始延迟之后,随后以给定的时间段首先启用的周期性动作; 那就是执行将在initialDelay之后开始,然后initialDelay+period ,然后是initialDelay + 2 * period等等。
这个是每次延迟时间,如果Sleep,会加时间延迟 -
另外一个方法能够保证每次循环是否Sleep间隔时间相同ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
创建并执行在给定的初始延迟之后首先启用的定期动作,随后在一个执行的终止和下一个执行的开始之间给定的延迟。 -
如何报异常
我们在不自己捕获异常,那么当我们代码有异常时交给线程池他会运行到代码异常那一行就不运行了,但是不会报异常!
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.schedule(()->{
int i=1/0;
System.out.println("你好");
},1, TimeUnit.SECONDS);
}
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.schedule(()->{
try{
int i=1/0;
System.out.println("你好");
}catch (Exception e){
e.printStackTrace();
}
},1, TimeUnit.SECONDS);
}
Fork/Join 线程池
是JDK1.7之后加入的线程池,适合能进行任务拆分的CPU密集型运算。
Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交个不同的线程来完成,进一步提升运算效率。
Fork/Join默认会创建和CPU核心线程数相等的线程池,无需我们去定义线程池大小。