线程池入门,
线程池入门,
什么是池,我们在开发中经常会听到有线程池啊,数据库连接池等等。
那么到底什么是池?
其实很简单,装水的池子就叫水池嘛,用来装线程的池子就叫线程池(废话),就是我们把创建好的N个线程都放在一个池子里面,如果有需要,我们就去取,不用额外的再去手动创建了
为什么要用线程池
按照正常的想法是,我们需要一个线程,就去创建一个线程,这样的想法是没错的,但是如果需要有N多个线程呢?那把创建线程的代码复制N多份?或者用for循环来创建?NO,这样不是不行,但是不好。
因为线程也是有生命周期的,创建与销毁线程都会对系统资源有很大的开销,创建线程需要向系统申请相应的资源,销毁线程又会对垃圾回收器造成压力
使用线程池的好处
线程池的适用场景
在实际开发中,如果需要5个以上的线程,那么就应该使用线程池来完成工作
线程池的创建与停止
我们先来说线程池的创建,我们都知道,在Java中的对象都是有构造方法的,有些可以使用无参的构造方法来创建,有些就需要使用有参的构造方法来创建,线程池的创建就必须要往构造方法中传入参数,我们就先来了解一下线程池中的构造参数都是一些什么含义吧,否则你怎么知道你创建的线程池是一个什么样的运行规则呢
线程池应该手动创建还是自动创建
建议手动创建,可以更加明确线程池的运行规则,避免资源耗尽的情况
我们看看自动创建会带来哪些问题
自动创建其实就是使用JDK已经创建好并提供给我们的一些线程池
newFixedThreadPool (中文意思:固定的线程池)
public class Main {
public static void main(String[] args) {
//创建一个newFixedThreadPool线程池,并设置它的线程数量为4
ExecutorService executorService = Executors.newFixedThreadPool(4);
//提交一千个任务去给它执行,结果就是它会不断打印线程1-线程4的名字
for(int i = 0 ; i < 1000 ; i ++){
executorService.execute(new test());
}
}
}
//打印当前线程的名字
class test implements Runnable{
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
前面我们说了,线程池的构造函数有好几个,为什么这里只要传一个就行了,并且前面还说了,有核心线程数和最大线程数,满足一定条件下会创建出额外的线程,那为什么只会打印线程1到线程 4的名字呢,我们去看看这个线程池的源码吧
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor( nThreads, //核心线程数
nThreads, //最大线程数
0L, //存活时间
TimeUnit.MILLISECONDS, //时间单位,这里是毫秒
//任务队列,这里使用的是无界队列
new LinkedBlockingQueue<Runnable>()
);
}
可以看出,内部new了一个ThreadPoolExecutor,里面的参数我也给打上注释了,核心线程数与最大线程数都是我们传进来的4这个参数,所以,无论有多少个任务进来,最多也就会有4个线程在进行工作,同时我们也看到了这里使用的无界队列,无界队列的特点就是没有容量限制,只要内存足够,可以无限扩展,所以不管有多少个任务进来,都会被存储到任务队列里面去。
仔细思考一下,使用这种线程池会有什么问题呢,当任务过多,线程处理不过来,就会不断的堆积到任务队列里面,这就造成了内存浪费,当队列向系统申请不到更多的内存时,还有新的任务提交过来,就会造成内存溢出。
可能会说,不是还有handler拒绝策略嘛,那我们再翻到前面看看,拒绝的条件是什么,当核心线程数不够用了,最大线程数也不够用了,并且队列已经满了的时候,新的任务才会被拒绝,这里是使用的无界队列,就不存在队列满了这么一个情况
newSingleThreadExecutor(单独的线程池)
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for(int i = 0 ; i < 1000 ; i ++){
executorService.execute(new test());
}
}
}
class test implements Runnable{
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
这个线程池不用传参数,从字面意思上就看看出,这个线程池里面只有一个线程,我们看看它的源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可以看出,这个线程池与newFixedThreadPool线程池很类似,只不过一个需要我们来指定核心线程数与最大线程数,一个不需要指定,已经写死了,就是一个,那么原理也就跟newFixedThreadPool线程池是一样的了。
newCechedThreadPool(缓存的线程池)
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i = 0 ; i < 1000 ; i ++){
executorService.execute(new test());
}
}
我们直接来看看它的源码吧
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, //核心线程数
//最大线程数
Integer.MAX_VALUE,
//存活时间
60L, TimeUnit.SECONDS,
//直接交换队列
new SynchronousQueue<Runnable>());
}
从源码我们可以看出这种线程池的特性,首先,核心线程数是0,也就是说,这个线程池中没有核心线程数,也就是没有可以一直存活的线程,最大线程数为Integer类型的最大值,可以说是没有上限的,你来多少任务我都接着,然后是存活时间,被设置为60秒,如果一个线程空闲的时间超过了60秒,就会被回收,然后是任务队列,这个队列前面介绍过了,里面没有容量,不会存放任务进去,每次一有任务就会立马交给线程去处理。
这个线程池存在什么问题,它会反复的创建与销毁线程,只要一有新的任务进来,就会立马创建一个线程去执行这个任务,如果执行完后没有新的任务交给它,就等待被销毁(等死)。
其次,也是有可能造成内存溢出的错误的
newScheduledThreadPool(支持定时或周期性任务执行的线程池)
这个线程有两种用法
我们先看第一种
public static void main(String[] args) {
//传入一个int类型的参数,这个参数代表线程池中核心线程的数量
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
//调用schedule方法,第一个参数是我们要执行的任务,第二个参数时间,第三个时间单位
//就是说,线程池在间隔5秒之后去执行我们提交的任务
executorService.schedule(new test(), 5, TimeUnit.SECONDS);
}
第二种
public class Main {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
//间隔一秒后开始执行任务,然后每隔3秒再次执行
executorService.scheduleAtFixedRate(new test(), 1, 3, TimeUnit.SECONDS);
}
}
线程池中的线程数量设定为多少比较合适
根据自己的业务场景不同有不同的规则
CPU计算密集型的任务(计算、加密、Hash等),最佳的线程数应该为CPU核心的1-2倍
耗时IO型任务(读写数据库、网络读写等),最佳的线程数可以为CPU核心数的很多倍,10、100甚至更多倍都可以的
计算公式:最佳线程数 = CPU核心数 * (1+平均等待时间/平均工作时间)
如何停止线程池
shutdown:优雅的停止线程
executorService.shutdown();
当我们调用了线程池的这个方法后,线程池就知道了我们需要它停止下来,同时,它并不会再去接收新的任务了
然后线程池就会把当前正在执行的任务和任务队列中的任务都执行完后,进行停止
isShutdown
这个方法返回一个boolean值,就是当我们对线程池调用了shutdown之后,我们想知道它到底有没有接收到
就可以调用这个方法,如果接收到了,会返回一个true,否则就是false
isTerminated
这个方法返回一个boolean值,这个方法用于检测,整个线程池是否已经停止工作了
awaitTermination
executorService.awaitTermination(3l,TimeUnit.SECONDS);
这个方法与isTerminated不同,这个方法是说,在我等待的这个时间内,线程池是否已经结束工作了
如果结束返回ture,否则false
shutdownNow:暴力停止线程
调用这个方法后,不管线程池中的任务是否还在执行,也不管任务队列中是否还有未执行的任务
线程池都会立刻停止工作,并且会把任务队列中未执行的任务进行返回
List<Runnable> runnableList = executorService.shutdownNow();
任务太多,怎么拒绝
拒绝的时机
拒绝的策略
最后一种拒绝策略是最好的,因为前面三种策略都是有损失的,要么新任务不执行,要么抛弃旧的任务,而最后一种没有损失,同时,这个线程老是给线程池去提交任务,那如果这个任务交由提交的线程去执行,那么这个线程就没有功夫去提交新的任务了,因为它已经被它提交的任务所占据了,只有等它的任务执行完之后,才会继续提交,这同时也降低了任务的提交速度
Executor家族的辨析
我们在前面创建线程池的代码中看到,一下是ExecutorService,一下又是Executors,那么它们之间的关系到底是什么呢,线程池不应该是ThreadPoolExecutor吗?怎么又是ExecutorService呢,别急,我们下面会详细介绍
我们从底层往上层讲
好,上面的我们已经介绍清楚了,那么Executors又是什么呢,Executors其实是一个工具类,里面有很多的方法,包括创建前面我们使用的那几种线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
这是我们前面所使用到的一种固定线程数的线程池,方法返回的是ExecutorService,
但是实际里面返回的是ExecutorService的子类:ThreadPoolExecutor
使用线程池的注意事项
相关文章
- 暂无相关文章
用户点评