1.线程池的作用
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果线程超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行;他的主要特点为:线程复用;控制最大并发数;管理线程。
使用线程池的好处:
第一:降低资源消耗;通过重复利用自己创建的线程降低线程创建和销毁造成的消耗;
第二:提高响应速度;当任务到达时,任务可以不需要等到线程创建完成能立即执行;
第三:提高线程的可管理性;线程是稀缺资源,如果无限的创建,不仅会消耗资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控
2.线程池架构图
Java中的线程池是通过 Executor 框架实现的,该框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor 这几个类.:
- Executor:一个接口,其定义了一个接收 Runnable 对象的方法 executor,其方法签名为 executor(Runnable command)
- ExecutorService:是一个比 Executor 使用更广泛的子类接口,其提供了生命周期管理的方法,以及可跟踪一个或多个异步任务执行状况返回 Future 的方法。
- AbstractExecutorService:ExecutorService 执行方法的默认实现。
- ScheduledExecutorService:一个可定时调度任务的接口。
- ScheduledThreadPoolExecutor:ScheduledExecutorService 的实现,一个可定时调度任务的线程池。
- ThreadPoolExecutor:线程池,可以通过调用 Executors 以下静态工厂方法来创建线程池并返回一个 ExecutorService 对象。
3.线程池的7大参数
java.uitl.concurrent.ThreadPoolExecutor 类是 Executor 框架中最核心的一个类。
ThreadPoolExecutor 有四个构造方法,前三个都是基于第四个实现。第四个构造方法定义如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
- corePoolSize:线程池的基本线程数。这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了 prestartAllCoreThreads()或者 prestartCoreThread()方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建 corePoolSize 个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。
- maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
- keepAliveTime:线程活动保持时间。线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
- unit:参数 keepAliveTime 的时间单位,有 7 种取值。可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
- workQueue:任务队列。用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列:
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
- threadFactory:创建线程的工厂。可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
- handler:饱和策略。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表示无法处理新任务时抛出异常。以下是 JDK1.5 提供的四种策略:
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
当然也可以根据应用场景需要来实现 RejectedExecutionHandler 接口自定义策略。如记录日志或持久化不能处理的任务。
4.线程池的种类:
目前 JDK 内置了以下5种线程池:
- Executors.newFixedThreadPool();
- Executors.newSingleThreadExecutor();
- Executors.newCachedThreadPool();
- Executors…newScheduledThreadPool();
- Executors.newWorkStealingPool(); //java8新出
下面分别对以上5种线程池做详细的说明:
1.Executors.newFixedThreadPool()
主要特点如下:
1.创建一个定长线程池,可控制线程的最大并发数,超出的线程会在队列中等待
2.newFixedThreadPool 创建的线程池 corePoolSize 和 MaxmumPoolSize 是相等的,它使用的阻塞队列是 LinkedBlockingQueue
使用示例:
public class MyThreadPoolDemo {
public static void main(String[] args) {
System.out.println("Fixed Thread Pool");
fixedThreadPool();
}
private static void fixedThreadPool() {
//一池5个线程
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//一般常用try-catch-finally
//模拟10个用户来办理业务,每个用户就是一个线程
try {
for (int i = 0; i < 9; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
Fixed Thread Pool
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-4 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-1 办理业务
pool-1-thread-5 办理业务
pool-1-thread-3 办理业务
2.Executors.newSingleThreadExecutor()
主要特点如下:
1.创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务都按照指定顺序执行
2.newSingleThreadExecutor 将 corePoolSize 和 MaxmumPoolSize 都设置为1,它使用的阻塞队列是 LinkedBlockingQueue
使用示例:
import java.util.concurrent.*;
/**
* 第四种使用Java多线程的方式,线程池
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
System.out.println("Single Thread Pool");
singleThreadPool();
}
private static void singleThreadPool() {
//一池1个线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 9; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
Single Thread Pool
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
3.Executors.newCachedThreadPool()
主要特点如下:
1.创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建新线程
2.newCachedThreadPool 将 corePoolSize 设置为 0; MaxmumPoolSize 设置为 Integer.MAX_VALUE;它使用的是 SynchronousQUeue,也就是说来了任务就创建线程运行,如果线程空闲超过60秒,就销毁线程
使用示例:
import java.util.concurrent.*;
/**
* 第四种使用Java多线程的方式,线程池
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
System.out.println("Cached Thread Pool");
cachedThreadPool();
}
private static void cachedThreadPool() {
//不定量线程
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 9; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
Cached Thread Pool
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-2 办理业务
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-4 办理业务
pool-1-thread-6 办理业务
pool-1-thread-5 办理业务
pool-1-thread-3 办理业务
4.Executors…newScheduledThreadPool()
主要特点如下:
创建一个定长线程池,支持定时及周期性任务执行。
它的线程数量是固定的,它可安排给定延迟后运行命令或者定期地执行,这类线程池主要用于执行定时任务和具有固定周期的重复任务。
使用示例:
public class ScheduledThreadPoolDemo {
private static void delay() {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(() -> System.out.println(Thread.currentThread().getName() + " 延迟 3 秒"), 3,
TimeUnit.SECONDS);
}
private static void cycle() {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.scheduleAtFixedRate(
() -> System.out.println(Thread.currentThread().getName() + " 延迟 1 秒,每 3 秒执行一次"), 1, 3,
TimeUnit.SECONDS);
}
public static void main(String[] args) {
delay();
cycle();
}
}
执行结果:
pool-1-thread-1 延迟 3 秒
pool-2-thread-1 延迟 1 秒,每 3 秒执行一次
pool-2-thread-1 延迟 1 秒,每 3 秒执行一次
pool-2-thread-2 延迟 1 秒,每 3 秒执行一次
pool-2-thread-1 延迟 1 秒,每 3 秒执行一次
pool-2-thread-3 延迟 1 秒,每 3 秒执行一次
5.Executors.newWorkStealingPool()
Java8 中,默认创建线程池的方法多了一个——Executors.newWorkStealingPool()
newWorkStealingPool 会创建一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行。
工作窃取不是什么 Java 独有的东西,.NET 的 TPL 库早就存在好几年了。所谓工作窃取,指的是闲置的线程去处理本不属于它的任务。
每个处理器核,都有一个队列存储着需要完成的任务。对于多核的机器来说,当一个核对应的任务处理完毕后,就可以去帮助其他的核处理任务。
/**
* Creates a work-stealing thread pool using all
* {@link Runtime#availableProcessors available processors}
* as its target parallelism level.
* @return the newly created thread pool
* @see #newWorkStealingPool(int)
* @since 1.8
*/
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
如上代码 newWorkStealingPool 方法本质上就是一个 ForkJoinPool。但是 ForkJoinPool 是 java7 中就用的东西,所以 newWorkStealingPool 其实也不是什么稀奇的东西。
5.线程池的工作原理:
6.拒绝策略
等待队列也已经排满了,再也塞不下新的任务了,同时,线程池的max也到达了,无法接续为新任务服务,这时我们需要拒绝策略机制合理的处理这个问题
JDK内置的4种拒绝策略:
- AbortPolicy(默认):直接抛出 RejectedException 异常阻止系统正常运行
- CallerRunPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,由调用者执行这些任务,从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常;如果允许任务丢失,这是最好的拒绝策略
下面对以上4种拒绝策略进行示例说明:
1.AbortPolicy
import java.util.concurrent.*;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool= new ThreadPoolExecutor(2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 0; i < 20; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-2 办理业务
pool-1-thread-4 办理业务
pool-1-thread-2 办理业务
pool-1-thread-2 办理业务
pool-1-thread-1 办理业务
pool-1-thread-5 办理业务
pool-1-thread-3 办理业务
pool-1-thread-4 办理业务
java.util.concurrent.RejectedExecutionException: Task thread.MyThreadPoolDemo$$Lambda$1/796684896@58ceff1 rejected from java.util.concurrent.ThreadPoolExecutor@2133c8f8[Running, pool size = 5, active threads = 3, queued tasks = 0, completed tasks = 8]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at thread.MyThreadPoolDemo.main(MyThreadPoolDemo.java:18)
分析:线程池最大容量为5+3=8;上述代码中创建了20个任务(0 ~ 19),当线程池务满的时候,继续创建新任务的话抛出异常
2.CallerRunPolicy
import java.util.concurrent.*;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool= new ThreadPoolExecutor(2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
try {
for (int i = 0; i < 20; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-5 办理业务
pool-1-thread-2 办理业务
pool-1-thread-4 办理业务
main 办理业务
pool-1-thread-5 办理业务
pool-1-thread-3 办理业务
pool-1-thread-5 办理业务
pool-1-thread-5 办理业务
pool-1-thread-1 办理业务
pool-1-thread-3 办理业务
main 办理业务
pool-1-thread-2 办理业务
pool-1-thread-2 办理业务
pool-1-thread-2 办理业务
分析:线程池最大容量为5+3=8;上述代码中创建了20个任务(0 ~ 19),当线程池务满的时候,继续创建新任务的话线程池会把任务回退给Main线程执行
3.DiscardOldestPolicy
import java.util.concurrent.*;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool= new ThreadPoolExecutor(2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
try {
for (int i = 0; i < 20; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-2 办理业务
pool-1-thread-2 办理业务
pool-1-thread-2 办理业务
pool-1-thread-2 办理业务
pool-1-thread-4 办理业务
pool-1-thread-4 办理业务
pool-1-thread-4 办理业务
pool-1-thread-4 办理业务
pool-1-thread-1 办理业务
pool-1-thread-5 办理业务
分析:线程池最大容量为5+3=8;上述代码中创建了20个任务(0 ~ 19),当线程池务满的时候,继续创建新任务的话线程池执行的任务不足20个,中间丢弃了等待时间最长的任务
4.DiscardPolicy
import java.util.concurrent.*;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool= new ThreadPoolExecutor(2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy()
);
try {
for (int i = 0; i < 20; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
执行结果:
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-3 办理业务
pool-1-thread-4 办理业务
pool-1-thread-1 办理业务
pool-1-thread-5 办理业务
分析:线程池最大容量为5+3=8;上述代码中创建了20个任务(0 ~ 19),当线程池务满的时候,继续创建新任务的话线程池执行的任务不足20个,直接丢弃了后续的任务
7.线程池的选择
工作中使用哪一种线程池?参考阿里巴巴java开发手册:
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
所以说应该尽量去使用自定义的线程池。
8.线程数量的设置
在创建线程池的时候,我们需要合理设置线程个数,那么应该如何设置呢?这里分为两种情况进行讨论:
1.CPU密集型
CPU密集的意思是该任务需要大量的运算而没有阻塞,CPU一直全速运行。因此,CPU密集型任务配置应该尽可能减少线程数量。可以用下面的公式进行线程数量的配置:
线程数量 = CPU核数 + 1
其中,使用 System.out.println(Runtime.getRuntime().availableProcessors());可以查看CPU核数。
2.IO密集型
由于 IO 密集型任务中线程并不是一直在执行任务,所以应该配置多一些线程数,可以去CPU核数的2倍作为线程数,或者使用以下公式:
线程数量 = CPU核数/(1-阻塞系数)
阻塞系数在0.8~0.9之间,一般可以取0.9
比如8核CPU:8/(1-0.9) = 80 个线程数