目录
- 1 多线程存在的性能问题
- 2 使用线程池创建线程的好处
- 3 线程池中各个参数的含义
- 4 线程池的四种拒绝策略
- 5 常见的线程池
- 6 如何确定线程的数量
- 7 线程池实现线程复用的原理—execute()方法
1 多线程存在的性能问题
多线程虽然可以提高我们的并发量,提高硬件的使用率,但是也存在非常大的性能问题。主要有以下三点
调度开销
- 上下文切换:我们的线程数一般都是大于我们的CPU核心数的,所以操作系统就需要跟据一定的算法,给每个线程分配一定的时间片,当时间片用完后就调度其他线程的使用CPU资源。整体的过程如下所示
CPU保存当前即将挂起线程的环境===>CPU找到并恢复即将开始的线程的运行环境
以此类推,这样就可以进行线程的上下文切换,这个过程是非常耗费时间的。如果线程数目非常大,就存在非常多的上下文切换,耗费了很多CPU计算时间。特别是在线程执行的任务的时间非常短时,甚至会造成线程上下文切换的时间比线程执行任务的时间还要长。 - 缓存失效:缓存是提高计算机性能非常重要的手段。线程A执行任务时会将所需要的数据加载到缓存中,但如果此时线程时间片用完了,需要进行线程调度,线程B就会占用线程A使用的CPU,但是此时缓存中的数据可能对于线程B是无效的,也就造成了缓存失效,需要再次从内存甚至磁盘中再次读取线程B需要的数据,这是十分耗时的
协作开销
在多线程环境下,我们需要保证共享资源的可见性,就会不得不在对数据进行修改后每次都直接从工作内存flush会 主存中,也就会降低我们整体的性能。
内存开销
我们的线程是需要占用一定的内存空间的,如果线程的数量过大,就会占用大量的内存,甚至会造成系统的崩溃。
2 使用线程池创建线程的好处
由于每有一个任务就创建线程存在上述的性能问题,就诞生了线程池来解决的这些问题。我们的解决思路就是可否用一定量的线程来执行我们的任务,实现线程的复用。
来看以下代码:
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 6; i++) {
int n = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行了第" + n + "个任务");
});
}
}
运行结果:
pool-1-thread-2执行了第1个任务
pool-1-thread-1执行了第0个任务
pool-1-thread-2执行了第2个任务
pool-1-thread-1执行了第3个任务
pool-1-thread-2执行了第4个任务
pool-1-thread-1执行了第5个任务
这里我们创建一个FixedThreadPool来执行我们的6个任务,从打印结果可以看到不管任务有多少,我们只有线程池中的两个线程来执行我们的任务,我们的线程池并不会无限制的增加线程来执行我们提交的任务。
所以,利用线程池就可以非常好的解决前面所说的多线程带来的性能问题,使用线程池的好处如下:
- 由于线程池中线程是可以复用的,所以我们可以不必创建很多的线程来执行我们的任务,也就减少了线程生命周期的开销
- 线程池可以统筹管理我们的CPU和内存资源。当线程不够时就创建,线程空闲时就销毁避免额外的内存开销
- 线程池可以方便我们管理提交的任务。
3 线程池中各个参数的含义
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数
- maximumPoolSize 最大线程数
- keepAliveTime + unit 空闲线程存活的时间
- workQueue 任务队列
- threadFactory 线程工厂,用于创建我们的线程
- handler 拒绝策略
4 线程池的四种拒绝策略
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
java为我们提供了四种拒绝策略,在我们的任务队列满和线程池中的线程达到最大值的时候就会执行
- AbortPolicy (也是默认的拒绝策略)
抛弃新进来的任务并抛出异常告诉程序哪个任务被抛弃了
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
- DiscardPolicy
直接抛弃新进来的任务而不做任何提示
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
- DiscardOldestPolicy
将任务队列队首的任务抛弃(不做提示),然后将新提交的任务提交给线程池
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
- CallerRunsPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
将任务交由任务提交者进行处理。
5 常见的线程池
- FixedThreadPool
核心线程数和最大线程数一样的线程池 - CacheThreadPool
核心线程数为0,最大线程数为Integer.MAX_VALUE,线程的keepAliveTime为60s - ScheduledThreadPool
能够按照指定时间间隔执行任务的线程池,核心线程数为由构造器传入,最大线程数为Integer.MAX_VALUE
这个线程池共有三个方法可以实现定时任务,区别如下:
- 在delay时间后执行任务,然后任务就清除了
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
- 按照一定频率执行某任务,在延迟initialDelay时间后,就开始每隔period执行一次该任务,并不考虑之前的任务是否已近完成
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
- 按照一定的时间延迟完成任务,在延迟initialDelay时间后,就开始执行第一次任务,然后需要等任务执行完成后,再等period后再执行下一次任务
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
- SingleThreadPool
核心线程数和最大线程数都为1 - SingleThreadScheduleExecutor
核心线程数为1的ScheduledThreadPool - ForkJoinPool
与上述五大线程池很不一样,一是这个线程池适合执行可以产生子任务的任务,而是这个线程池除了自己有一个任务队列以外,每一个线程也有都一个属于自己的任务队列
6 如何确定线程的数量
- 对于平均等待时间长的任务,也就是IO密集型的任务我们可以设置较多的线程数量,因为IO密集型的任务对于每个任务占用CPU的时间短,但等待IO的时间很长,多以可以多设置一些线程
- 对于平均执行时间长的任务,也就是CPU密集型的任务,我们设置的线程数一般为CPU核心数的1到2倍,由于CPU密集型任务需要占很长的CPU时间来进行计算,若创建线程的数目过多,会导致非常频繁的线程上下文切换使得系统性能降低
7 线程池实现线程复用的原理—execute()方法
public void execute(Runnable command) {
//传入的任务为null则直接抛出异常
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//计算当前的工作线程数,并与核心线程数进行比较
if (workerCountOf(c) < corePoolSize) {
//当前工作线程数小于核心线程数则直接调用addWork()方法
//true表示若当前线程数少于核心线程数则增加线程执行提交的任务
//false表示若单签线程数少于最大线程数则增加线程执行提交的任务
if (addWorker(command, true))
return;
c = ctl.get();
}
//走到这儿说明当前线程池中的线程数已经大于核心线程数了
//当前线程池处于运行状态并且向任务队列中添加任务成功
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//再次进行判断,如果线程池已经关闭则移除刚刚提交的任务并执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
//到这儿说明线程池在运行,此时应判断一下当前线程池中是否有线程,若无则应创建线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//执行到这儿说明线程池已经关闭或者任务队列已经满了且线程池数量已经达到核心线程数,此时添加新线程,
//若添加新线程失败则执行决绝策略
else if (!addWorker(command, false))
reject(command);
}
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//执行到这儿说明线程池已经关闭或者任务队列已经满了且线程池数量已经达到核心线程数,此时添加新线程,
//若添加新线程失败则执行决绝策略
else if (!addWorker(command, false))
reject(command);
}