前面一直聊了三种创建线程的方法,通过类Thread,接口Runnable,接口Callable。
当然还有一种那就是通过线程池,对于这个名字很容易想到的是连接池,虽然是两个东西,但是其优点差不多。
线程池的作用就是控制运行的线程数量,处理过程中任务放入队列,然后在线程创建后启动这些任务。
如果线程数量超过线程池最大数量的时候,超初的线程就去判断等候区,等正在运行的线程执行完毕之后,在冲队列中取出任务来执行。
其特点如下:
降低资源消耗,毕竟可以重复利用已创建好点线程。所以节约了线程创建和销毁的性能消耗。
提高了线程的可管理性,毕竟线程是稀有资源,无效创建线程不但不会提高程序的运行效率还会降低效率以及系统的稳定性。 使用线程池可以统一调配监控。
Java中的线程池是通过Executor框架实现的,该框架使用Executor,Executors,ExecutorService,ThreadPoolExcutor几个类。还是老规矩盗一张图:
但是本篇不涉及一些底层原理,而是主要聊是如何用线程池,以及在使用线程池时候实际是如何用的。
在创建线程池的时候Java为了方便程序员使用,通过Executors这个工具类提供一些线程池传创建的方法:
可以看出里面有很多方法,现在只聊三个提供的创建线程池的方法:
newFixedThreadPool
这个会创建一个线程池,然后规定可以同时运行的有多少线程。然后进行重复使用创建的线程。
newSingleThreadExecutor
这个线程池有些不像是线程池,因为线程池中只有一个线程,通俗的说就是单线程池子。
newCachedThreadPool
这个线程池,不过这个线程池是弹性的,也就是随着线程的多少会自我调整池子大小。
然后看三个创建线程池的方法,然后返回的都是ExecutorService类,所以说需要通过ExecutorService的方法进行控制池子的:
其父类还有一个方法:
现在开始演示:
这个可以看出就是创建一个同时可以运行某几个线程的池子,岂有两种方法:
newFixedThreadPool(int nThreads);//线程可以同时运行的数目 nThreads
newFixedThreadPool(int nThreads, ThreadFactory threadFactory);// 线程可以同时运行的数目 nThreads ,threadFactory就是创建线程工厂返回线程而已
现在开始演示:
public class test {
public static void main(String[] args) {
ExecutorService threadPool= Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
threadPool.execute(()->{
System.out.println("输入的线程是"+Thread.currentThread().getName());
});
}
threadPool.shutdown();
}
}
可以看出其运行的一共有三个线程,无论把线程添加多少起运行的线程一直是3条或者说是同时运行的是3条线程。
看一下起创建的方式:
然后用如下代码演示:
public class test {
public static void main(String[] args) {
ExecutorService threadPool= Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
// 继承的是父类的方法execute
threadPool.execute(()->{
System.out.println("输入的线程是"+Thread.currentThread().getName());
});
}
// 需要吧线程池关闭
threadPool.shutdown();
}
}
可以看出其运行的是一个线程。
这个线程池很有意思,会动态调整的先看方法:
然后演示:
public static void main(String[] args) {
ExecutorService threadPool= Executors.newCachedThreadPool();
// 一个是30 然后再试一下50 然后看截图
for (int i = 0; i <30; i++) {
// 继承的是父类的方法execute
threadPool.execute(()->{
System.out.println("输入的线程是"+Thread.currentThread().getName());
});
}
// 需要吧线程池关闭
threadPool.shutdown();
}
}
电脑内存不同,可能结果不同,本人电脑设置30个线程,最后发现线程池是14个
然后运行50个线程试一下:本人电脑运行时19个
说实话这样用线程池很方便,但是一般不会如此用,而是通过ThreadPoolExecutor。
上面介绍的三个创建线程池的方式,然后看一下源码会发现:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
发现没有提创建线程池都是通过new ThreadPoolExecutor这个对象而创建的。为什么用这个,咱看一下阿里的规范就明白了:
阿里的代码规范:
【强制】线程池不允许使用Executors 去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规 则,规避资源耗尽的风险。
说明: Executors返回的线程池对象的弊端如下:
FixedThreadPool和 SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。也可能导致栈溢出。
CachedThreadPool和 ScheduledThreadPool
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。也可能导致栈溢出。
Integer.MAX_VALUE : java int 类型整数的最大值是(2 的 31 次方) - 1 = 2147483648 - 1 = 2147483647(21亿多) (java中 int类型 4Byte(字节) = 32bit(位))
所以明白了为什么要使用ThreadPoolExecutor来创建线程池了吧,现在开始聊一下ThreadPoolExecutor。
然后翻看ThreadPoolExecutor的构造方法:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
其有七个参数,现在依次聊一下七个参数是什么意思:
参数 | 描述 |
---|---|
int corePoolSize | 线程池里面可以共享多少个线程数量,也就是同时可以运行的线程数量,也就是常驻线程数量 |
int maximumPoolSize | 线程池其最大的线程池数量,也就是如果现有的线程池的线程不够用,最多可以调用线程的数目 |
long keepAliveTime | 这个是线程的存储时间,如果设置了线程可以动态调整,自然如果线程足够了,自然会有一个时间来将其释放来解放性能,不过这个数字是时间单位之前的量词,所以需要与后面的一个参数一起用,那就是时间单位。 |
TimeUnit unit | 这个时间单位。 |
BlockingQueue | 阻塞线程,这个前面一篇聊了这个东西传送阵毕竟是一个运行的池子,而超过池子的线程放在的缓存队列中。 |
ThreadFactory threadFactory, | 线程工厂,也就是创建线程的工厂。 |
RejectedExecutionHandler handler | 这个是一个拒绝策略,下面补充 |
整体的要给架构图如下:
其中对于拒绝策略需聊一下:
看一下官网:
可以看出岂有四个拒绝策略,现在依次聊一下拒绝测试的不同。
AbortPolicy异常时默认异常:直接抛出rejectedExecution异常。
因为这个的意思就是所有的队列以及设置了最大的线程数量也满了(最大都满了意味着核心线程池的数量肯定也满了),然后还要继续放入数据,直接返回错误。
CallerRunsPolicy拒绝策略更有有意思:其时就是调用者运行的机制。简单的说就是某线程调用了线程池发现什么都满了,然后直接退回去说你调用的,这里已经满载了你自己运行吧。如果现在在就执行将返回的线程运行完,如果在回来之前调用的线程已经消失了那就直接舍弃这个任务。有点像是邮局某种原因被退的信,如果退信地址都被拆了那就直接将信废弃。
DiscardOldestPolicy这个策略就有点喜新厌旧了,如果信赖的线程发现线程池已满载,然后就看阻塞队列中放入最早的线程之间扔了,然后将其放在队列之中,然后尝试或者说是等着向线程池中执行。
DiscardPolicy策略其实更有意思其默认是抛弃无法处理的业务,对于这种业务不做任何处理也不抛出异常,直接舍弃。对于一些允许业务丢失从场景是最好的一种策略,毕竟效率高。
现在看是搞一个自定义的线程池是如何使用的,现在开始
不过首先说一下看起构造方法:
可以看出通过自定义线程池不一定非要写7个参数,其中前面5个必写,后2个可以选择其中1个或者2个都写。
其中拒绝策略默认为AbortPolicy,线程工厂默认自动创建。
不过咱们演示直接就是7个参数来演示。
public class test {
public static void main(String[] args) {
ThreadPoolExecutor threadpool= new ThreadPoolExecutor(3,
3,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
String threadname= String.valueOf(UUID.randomUUID());
return new Thread(r,threadname);
}
},
// 因为聊的四种策略是ThreadPoolExecutor类中的内部类,所以需要这样创建
new ThreadPoolExecutor.AbortPolicy()
);
System.out.println(threadpool);
}
}
这样可以看输出的池子是什么
可以看出池子本身没有运行,所以需要提交线程进入,才可以的。
所以如下测试:
public class test {
public static void main(String[] args) {
ThreadPoolExecutor threadpool = new ThreadPoolExecutor(3,
3,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
String threadname = String.valueOf(UUID.randomUUID());
return new Thread(r, threadname);
}
},
// 因为聊的四种策略是ThreadPoolExecutor类中的内部类,所以需要这样创建
new ThreadPoolExecutor.AbortPolicy()
);
System.out.println("添加线程到线程池之前:"+threadpool);
for (int i = 0; i < 10; i++) {
threadpool.execute(new testThread());
}
System.out.println("添加线程到线程池之后:"+threadpool);
threadpool.shutdown();
}
}
class testThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
报错了,为什么?
因为采用的是AbortPolicy策略,所以不够自然会报错,因为核心线程和最大线程为3,而阻塞队列最多放3个,也就是6个,而此时我们放入10个自然会报错。
可以把最大线程池放大或者把阻塞队列放大,再或者就是换一个拒绝策略,虽然不会报错了,但是也会根据自己的要求而舍弃一些策略。
比如把策略换成:
// 放弃最早放入的阻塞队列的线程
new ThreadPoolExecutor.DiscardOldestPolicy()
会发现最后只有9个线程会舍弃一个线程。
如果把最大的线程池子换成10,而且策略还是AbortPolicy的结果呢?
直接运行完毕,没有问题,还可以注意线程池变成了4个,其核心(或者说常驻)线程为3个。
其方法有很多,不再一一演示,而是简单的把官方方法黏贴一下了:
本篇也就是简单说了一下自定义线程池如何使用,算是一个了解其调用的使用。