线程池
- 线程池是什么
- ThreadPoolExecutor
- 模拟实现线程池
- 结语
线程池是什么
假设我们要频繁的创建线程和销毁线程,但是创建线程和销毁线程是有成本的.
所以我们可以提前创建一批线程,后面需要使用的时候,直接拿就可以了,这就是线程池.
当线程不再使用的时候,就归还到池子里.
为什么从线程池里取比在系统里创建线程更加高效呢?
用系统去创建线程,需要调用系统api,进一步有系统内核完成线程的创建.
(内核是给所有线程提供服务的,这是不可控的)
如果是从线程池里取,上述在内核里的操作都已经提前做好了,取线程的过程,就变为了纯用户态(可控).
在java标准库中,也提供现成的线程池供我们使用.
public static void main(String[] args) {
// Executors: 工厂类 newFixedThreadPool(int): 工厂方法
// 工厂模式: 一般创建对象都是通过new来调用构造方法
// 创建了一个固定数量的线程池
ExecutorService service = Executors.newFixedThreadPool(4);
// 创建一个线程树木动态变化的线程池
//Executors.newCachedThreadPool();
// 创建单个线程(比原本系统内核创建线程更简单)
//Executors.newSingleThreadExecutor();
// 创建计时器线程.可能是由多个线程共同执行所有的任务
//Executors.newScheduledThreadPool(2);
for (int i = 0; i < 20; i++) {
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello Executors");
}
});
}
}
// 这里可能会用创建的全部线程去执行,打印20个hello Executors.
什么是工厂模式呢?
一般创建对象都是通过new来调用构造方法,但是构造方法的名字固定就是类名,
有的类就需要多种不同的构造方法,因为构造方法的名字固定,就只能使用方法重载来实现了.
可是还有不能使用方法重载的场景,比如数学中的一个坐标点,
可以使用笛卡尔坐标系的方式,也可以使用极坐标的方式,它们参数的个数和类型相同,无法
构成重载.
当我们使用工厂模式时,不用使用构造方法了,使用普通的方法来构造对象,这样方法的名字就可以是任意的了.由于普通方法的目的是创建对象,这样的方法一般是静态的.
ThreadPoolExecutor
除了上述的线程池之外,标准库还提供了接口更丰富的线程池类: ThreadPoolExecutor.
我么来看看java文档中ThreadPoolExecutor的构造方法,并来学习线程池构造方法的参数和含义.
- int corePoolSize : 核心线程数, 在ThreadPoolExecutor里面的线程个数,并非是固定不变的,会根据当前任务的情况动态发生变化,至少得有corePoolSize 线程,哪怕线程池中一点任务也没有.
- int maximumPoolSize: 最大线程数: maximumPoolSize表示最多的线程数,不能比这个数目更多了.
- long keepAliveTime, TimeUnit unit : 分别表示时间和单位, 比如3000, ms, 这时就是3s.当线程超过制定时间阈值后就可以销毁了.
- BlockingQueue workQueue: 线程中有很多任务,这些任务可以用阻塞队列来管理.
- ThreadFactory threadFactory: 工厂模式,通过这个工厂类来创建线程.
- RejectedExecutionHandler handler(非常重要,重点掌握): 拒绝方式/拒绝策略.我们知道,线程池中有一个阻塞队列,当阻塞队列满的时候,继续添加任务,该如何应对???
(1) ThreadPoolExecutor.AbortPolity: 直接抛出异常,线程池就不干活了.
(2) ThreadPoolExecutor.callerRunsPolity : 谁是添加这个新任务的线程,谁就去执行这个任务.
(3) ThreadPoolExecutor.DiscardOldestPolity: 丢弃最早的任务,执行新的任务.
(4) ThreadPoolExecutor.DiscardPolity: 直接把新的任务丢弃掉.
模拟实现线程池
这里我们实现一个固定数量的线程池:
class MyThreadPool {
private final BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
// 添加任务
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
// 创建一个固定数量的线程池
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Thread thread = new Thread(() -> {
while (true) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
}
}
public class Demo26 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(4);
for(int i = 0; i < 1000; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello pool");
}
});
}
}
}
在我们创建线程池的时候,线程个数是哪来的?
- 有的线程的工作是"CPU密集型", 线程的工作全是运算.大部分的工作是在CPU上完成的,CPU得给它安排核心去完成工作才可以有进展.如果CPU是N个核心,当线程数量也是N的时候.这是理想情况,每个核心上一个线程.如果有很多的线程,就会阻塞等待.
- 有的线程是的工作,是"IO密集型", 会涉及大量的等待时间,就算线程数量多一点,也不会给CPU造成太大的负担.
在实际开发中,往往通过尝试不同的线程数,来找到合适的线程数,找到性能和系统资源开销比较均衡的数值.
结语
本篇博客总结了线程池相关的知识,满满的干货,希望有收获的小伙伴多多支持!