目录
引入线程池的原因
介绍标准库中线程池的参数(高频面试题)
实际开发中,核心线程数设置为多少才合适?
线程池的使用
自己实现一个简单的线程池
像线程池/常量池/内存池/进程池等等,这些池的思想都是一样的——提高效率。
引入线程池的原因
并发编程使用多进程,线程比进程更轻量,在频繁创建销毁方面更有优势,随着时代的发展对频繁有了新的定义,现有的要求下,频繁的创建销毁线程使开销越来越明显了,那么对于这个问题该如何进行优化呢?
(1)线程池 (2)协程(也可以叫纤程,可以理解为比线程更加轻量的程序)
线程池/协程能提高效率的原因:
常规时,线程的创建销毁是要由用户态+内核态配合完成,当引入线程池/协程后可以只由用户态就可以完成,不需要和内核态配合完成。直接调用api创建销毁线程,这个过程需要内核完成,而内核完成的工作很多时候是不太可控的(可能内核还要先做别的工作,这个事情是不可控的),如果使用线程池,提前把线程创建好放到用户态中的数据结构,用的时候直接在线程池中获取,用完了再放回线程池,这个过程完全是用户态代码,不需要和内核进行交互。
协程本质也是纯用户态代码,规避内核操作,不同的是用一个内核的线程来表示多个协程,纯用户态,进行协程之间的调度。
介绍标准库中线程池的参数(高频面试题)
标准库线程池使用ThreadPoolExecutor,使用起来比较复杂,构造方法中包含很多的参数
我们一个一个来看:
(1)corePoolSize 核心线程数,在创建线程池时可以自己设定包含多少核心线程
(2)maximumPoolSize 最大线程数,是核心线程+非核心线程的总数,一个线程池刚创建时只包含核心线程数这么多的线程,线程池提供一个submit方法往里面添加任务,每个任务都是一个runnable对象,如果当前的任务较少,当前核心线程可以处理好这些任务,那么此时线程池中就只有核心线程在工作;如果任务较多,核心线程干不过来了,这个时候线程池就会创建新的线程来支撑更多的工作,这个过程称为——动态扩展。新创建的线程数就是非核心线程,新创建的非核心线程加上核心线程不能超过最大线程数。
过了一段时间后任务没那么多了,部分的非核心线程就会被GC回收掉,这样既保证了任务多时工作的效率,也保证了任务少时系统的开销小。
实际开发中,核心线程数设置为多少才合适?
取决于电脑配置(cpu核心数)与程序的实际特点,可以分为两个大类:
1、cpu密集型:代码要完成的逻辑都是要通过cpu干活来完成,比如说如下这段代码:
int count = 0;
while(true){
count++;
}
形如这种代码逻辑都是在进行逻辑判断/算术运算/循环判定等等,这样的逻辑运行后可以立马吃满一个cpu,如果程序是cpu密集型的,线程数就不应该超过cpu核心数,因为超过了也是浪费。
2、IO密集型:代码大部分时间都在等待IO,等待IO是不消耗cpu的
例如这样的代码:
Thread t1 = new Thread(()->{
Scanner scanner = new Scanner(System.in);
int num = scanner.nextInt();
System.out.println(num);
});
线程最消耗时间的部分是等待输入。输入输出/sleep/操作硬盘/操作网络等操作都可能会让线程等待IO。
如果你的代码是IO密集型,那它的瓶颈不在cpu上,每个cpu只消耗一点点,线程数取决于其他方面(网络程序/网卡带宽),此时可以在硬件限制下多创建些线程。
小结:以上两种情况都太理性化了,在实际开发中大多是一个程序将cpu密集型与IO密集型都包含了,对于线程数目设置多少最合适这个问题,答案是:根据实际实验找出适合的值,对程序进行性能测试,通过设定不同的线程数,根据实际程序的响应速度和系统开销权衡利弊,最终得到一个你觉得最合适的线程数。
继续分析线程池构造方法的参数列表
(3)keepAliveTime:允许非核心线程空闲时存活的最大时间,数值
(4)Timeunit:上面存活时间的单位
(5)BlockingQueue<Runnable>:线程池的任务队列。线程池提供一个submit方法,让线程将任务交给线程池,线程池内部有一个类似队列的数据结构来存储runnable对象,要执行的任务就是runnable对象对应的run方法里的内容。
(6)ThreadFactory:线程工厂。工厂模式也是一种经典的设计模式,主要是为了解决基于构造方法创建对象会产生错误的逻辑。这个线程工厂主要是在批量创建线程时提前设置好了属性,线程工厂在它的方法中提前把线程的属性初始化好了。
重点:(7)RejectedExecutionHandler:拒绝策略
假设:如果当前任务队列满了,仍然要添加任务,那么线程池就会给出四种拒绝策略来告诉线程异常,(a)ThreadPoolExecutor.AbortPolicy:直接抛出异常,让程序员知道此时线程池已经满了
(b)ThreadPoolExecutor.CallerRunsPolicy:哪个线程向线程池中添加任务就由哪个线程自己执行,线程池本身不管了。
(c)ThreadPoolExecutor.DiscardOldestPolicy:丢弃最老的任务,将新的任务放到任务队列中排队。
(d)ThreadPoolExecutor.DiscardPolicy:丢弃最新的任务,按照原有的节奏继续执行,无视新任务。
线程池的使用
标准库的ThreadPoolExecutor使用起来比较费劲,标准库自己提供了几个工厂类,对于上述进程池进一步封装了,如果只是想简单使用一下,那工厂类就够了;如果想更精细的控制就使用原生的ThreadPoolExecutor。
这些是标准库提供的创建线程池的工厂方法:
这四个工厂方法的介绍放在了代码的注释中
Executors.newCachedThreadPool(); //创建一个普通线程池,会根据任务数量自行扩容
Executors.newFixedThreadPool(10);//创建一个固定线程数量的线程池
Executors.newSingleThreadExecutor();//创建一个只包含单线程的线程池
Executors.newScheduledThreadPool(10);//创建一个固定线程数量,但延迟执行的线程池
栗子:使用第一种方法创建一个线程池并添加任务
Thread t2 = new Thread(()->{
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
int count = i;
service.submit(new Runnable() {
@Override
public void run() {
System.out.println(count+" "+Thread.currentThread().getName());
}
});
}
});
t2.start();
截取的一部分运行结果,可以看出自动创建了多个线程来进行该任务。
自己实现一个简单的线程池
一个线程池要有:(1)若干个线程,具体多少个看调用时想设置为多少个 (2)有任务队列,元素为runnable (3)submit方法,将任务添加到任务队列
第一步:实现自己的线程池类,要有任务队列->BlockingQueue;要有最大线程数与核心线程数,在构造方法中最大线程数就是调用时给定的值,核心线程数需要用循环创建线程,每个线程都要执行任务;submit方法将任务添加到队列中。初步框架已经列好,代码为:
class MyThreadPool{
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
private int maxPoolSize = 0;
public MyThreadPool(int corePoolSize,int maxPoolSize){
this.maxPoolSize = maxPoolSize;
for (int i = 0; i < corePoolSize ; i++) {
Thread t = new Thread(()->{
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
}
void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
}
第二步:上述代码的问题(1)每个线程创建完成后需要不停的执行工作,所以try catch内的任务应该使用 while循环进行 (2)submit中应该对任务数设置一个阈值,超过这个值说明任务数量太多了需要创建新的线程来工作,但还需要用总线程数来限制,创建的线程数不得大于最大线程数,想记录任务数量可以使用顺序表这样的数据结构来记录数量。优化后的代码为:
class MyThreadPool{
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
private int maxPoolSize = 0;
List<Runnable> list = new ArrayList<>();
public MyThreadPool(int corePoolSize,int maxPoolSize){
this.maxPoolSize = maxPoolSize;
for (int i = 0; i < corePoolSize ; i++) {
Thread t = new Thread(()->{
try {
while (true){
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
list.add(t);
}
}
void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
if(queue.size() >= 100 && list.size() < maxPoolSize){
Thread t = new Thread(()->{
while (true){
try{
Runnable task = queue.take();
task.run();
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
t.start();
}
}
}
这样,一个简单的类似线程池的代码就创建完了。
道阻且长,行则将至
感谢观看