目录
1.1 newFixedThreadPool:固定大小线程池
1.2 newSingleThreadExecutor:单线程线程池
3.4 DiscardOldestPolicy:丢弃队列中最旧的任务
引言
在多线程编程中,线程池是一种非常重要的技术,它可以有效地管理线程的生命周期,减少线程创建和销毁的开销,提高系统的性能和稳定性。Java通过java.util.concurrent
包提供了强大的线程池支持。本文将详细介绍线程池的三大创建方法、七大核心参数以及四大拒绝策略,帮助你深入理解并正确使用线程池。
池化技术
前面提到一个名词——池化技术,那么到底什么是池化技术呢 ?
池化技术简单点来说,就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化 技术可以大大的提高资源的利用率,提升性能等。
在编程领域,比较典型的池化技术有:
线程池、连接池、内存池、对象池等
主要来介绍一下其中比较简单的线程池的实现原理,希望读者们可以举一反三,通过对线程池的理解, 学习并掌握所有编程中池化技术的底层原理。
我们通过创建一个线程对象,并且实现Runnable接口就可以实现一个简单的线程。可以利用上多核 CPU。当一个任务结束,当前线程就接收。
但很多时候,我们不止会执行一个任务。如果每次都是如此的创建线程->执行任务->销毁线程,会造成 很大的性能开销。
那能否一个线程创建后,执行完一个任务后,又去执行另一个任务,而不是销毁。这就是线程池。
这也就是池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接 获取,避免多次重复创建、销毁带来的开销。
为什么使用线程池?
10 年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球 ,CPU 需要来回切换。 现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。
线程池的优势:
线程池做的工作主要是:控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这 些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中 取出任务来执行。
它的主要特点为:线程复用,控制最大并发数,管理线程。
第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系 统的稳定性,使用线程池可以进行统一分配,调优和监控。
一、线程池的三大创建方法
Java中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor ,Executors, ExecutorService,ThreadPoolExecutor 这几个类。
Java提供了三种常用的线程池创建方法,通过Executors
工具类可以快速创建线程池。这些方法适用于不同的场景,但需要注意它们的特性和潜在问题。
1.1 newFixedThreadPool
:固定大小线程池
-
特点:创建一个固定大小的线程池,线程池中的线程数量始终不变。
-
适用场景:适用于负载比较稳定的服务器,能够控制线程的最大并发数。
-
潜在问题:队列使用的是无界队列(
LinkedBlockingQueue
),如果任务提交速度远大于任务处理速度,可能导致内存溢出。
public class MyThreadPoolDemo {
public static void main(String[] args) {
// 池子大小 5
ExecutorService threadPool =
Executors.newFixedThreadPool(5);
try {
// 模拟有10个顾客过来银行办理业务,池子中只有5个工作人员受理业务
for (int i = 1; i <= 10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown(); // 用完记得关闭
}
}
}
1.2 newSingleThreadExecutor
:单线程线程池
-
特点:创建一个只有一个线程的线程池,所有任务按顺序执行。
-
适用场景:适用于需要保证任务顺序执行的场景。
-
潜在问题:同样使用无界队列,可能导致内存溢出。
public class MyThreadPoolDemo {
public static void main(String[] args) {
// 有且只有一个固定的线程
ExecutorService threadPool =
Executors.newSingleThreadExecutor();
try {
// 模拟有10个顾客过来银行办理业务,池子中只有1个工作人员受理业务
for (int i = 1; i <= 10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown(); // 用完记得关闭
}
}
}
1.3 newCachedThreadPool
:缓存线程池
-
特点:创建一个可缓存的线程池,线程池中的线程数量不固定,空闲线程会被回收,新任务会创建新线程。可扩容,遇强则强。
-
适用场景:适用于执行大量短期异步任务的场景。
-
潜在问题:线程数量没有上限,可能导致线程数量过多,耗尽系统资源。
public class MyThreadPoolDemo {
public static void main(String[] args) {
// 一池N线程,可扩容伸缩
ExecutorService threadPool =
Executors.newCachedThreadPool();
try {
// 模拟有10个顾客过来银行办理业务,池子中只有N个工作人员受理业务
for (int i = 1; i <= 10; i++) {
// 模拟延时看效果
// try {
// TimeUnit.SECONDS.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown(); // 用完记得关闭
}
}
}
二、线程池的七大核心参数
虽然Executors
提供了快速创建线程池的方法,但在实际开发中,更推荐使用ThreadPoolExecutor
手动创建线程池,以便更好地控制线程池的行为。
查看三大方法的底层源码,发现本质都是调用了 new ThreadPoolExecutor ( 7 大参数 )
源码:
// 源码
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
2.1 corePoolSize
:核心线程数
-
作用:线程池中始终保持存活的线程数量。
-
特点:即使线程处于空闲状态,也不会被回收,除非设置了
allowCoreThreadTimeOut
。
2.2 maximumPoolSize
:最大线程数
-
作用:线程池中允许的最大线程数量。
-
特点:当任务队列已满且核心线程都在忙时,线程池会创建新线程,直到线程数达到
maximumPoolSize
。
2.3 keepAliveTime
:线程空闲时间
-
作用:当线程池中的线程数量超过
corePoolSize
时,空闲线程的存活时间。 -
特点:超过空闲时间的线程会被回收,直到线程数降到
corePoolSize
。
2.4 unit
:空闲时间单位
-
作用:
keepAliveTime
的时间单位,如TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
等。
2.5 workQueue
:任务队列
-
作用:用于存放待执行任务的阻塞队列。
-
常用队列:
-
LinkedBlockingQueue
:无界队列,可能导致内存溢出。 -
ArrayBlockingQueue
:有界队列,需要指定队列大小。 -
SynchronousQueue
:不存储元素的队列,每个插入操作必须等待一个移除操作。
-
2.6 threadFactory
:线程工厂
-
作用:用于创建新线程的工厂。
-
特点:可以自定义线程的名称、优先级等属性。
2.7 handler
:拒绝策略
-
作用:当任务队列已满且线程数达到
maximumPoolSize
时,如何处理新提交的任务。 -
四大拒绝策略:见下文。
三、线程池的四大拒绝策略
首先要理解ThreadPoolExecutor 底层工作原理:
当线程池无法处理新任务时(阻塞队列已满且线程数达到maximumPoolSize
),会触发拒绝策略。
举例:8个人进银行办理业务
1、1~2人被受理(核心大小core)
2、3~5人进入队列(Queue)
3、6~8人到最大线程池(扩容大小max)
4、再有人进来就要被拒绝策略接受了。
流程:
一. 在创建了线程池后,开始等待请求。
二. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务:
2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列:
3. 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非 核心线程立刻运行这个任务;
4. 如果队列满了且正在运行的线程数量大于或等于1Size,那么线程池会启动饱和拒绝策略来执 行。
三. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
四. 当一个线程无事可做超过一定的时间(keepA1iveTime)时,线程会判断:
如果当前运行的线程数大于coreP佣1Size,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
Java提供了四种内置的拒绝策略:
3.1 AbortPolicy
:直接抛出异常
-
特点:直接抛出
RejectedExecutionException
,阻止系统正常运行。 -
适用场景:需要快速失败并明确知道任务被拒绝的场景。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
3.2 CallerRunsPolicy
:调用者运行
-
特点:将任务回退给调用者线程执行。
-
适用场景:适合需要保证任务一定被执行的场景。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
3.3 DiscardPolicy
:直接丢弃任务
-
特点:直接丢弃新提交的任务,不做任何处理。
-
适用场景:适合对任务丢失不敏感的场景。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy()
);
3.4 DiscardOldestPolicy
:丢弃队列中最旧的任务
-
特点:丢弃队列中最旧的任务,然后尝试重新提交新任务。
-
适用场景:适合允许丢弃旧任务的场景。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
思考:
线程池用哪个?生产中如何设置合理参数?
在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多? 坑
答案是一个都不用,我们工作中只能使用自定义的;Executors 中 JDK 已经给你提供了,为什么不用?
线程是否越多越好?
一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu 核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。
但是如果线程远远超出cpu核心数量 反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间 的。
因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了。
如果是一个磁盘或网络为主的程序(IO密集型)。一个线程处在IO等待的时候,另一个线程还可以在 CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的 话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。
所以开多线程,比 方说多线程网络传输,多线程往不同的目录写文件,等等。
此时 线程数等于IO任务数是最佳的。
四、总结
线程池是多线程编程中的核心工具,合理地使用线程池可以显著提高系统的性能和稳定性。通过本文的介绍,你应该已经掌握了线程池的三大创建方法、七大核心参数以及四大拒绝策略。在实际开发中,要具体需求手动配置线程池,避免使用Executors
的默认方法,从而更好地控制线程池的行为。