一、为什么要引入线程池?
虽然对比进程,线程已经很轻量了,创建销毁调度线程都更高效。但是随着并发程度的提高,我们对于性能要求的标准也越来越高,当我们需要频繁创建销毁调度线程时,就发现线程也没有那么轻量。于是就引入了线程池,来进一步提高效率。
二、线程池的理论知识
事先把需要的线程创建好,放到“线程池”中,后面使用的时候,直接从池里取;用完了,直接放回池中。从池中取和放回池中,这两个操作,比创建销毁线程更高效。
为什么呢?
因为从池中取和放回池中,是程序员自己的代码就可以实现的,想干啥,怎么干,都由程序员自己决定的,是用户态。
而创建和销毁线程,是由操作系统内核完成的,内核只会给我们提供一些API(也就是系统调用),我们只能够通过系统调用,让内核去完成创建或销毁线程,但我们并不清楚内核都要做哪些工作,身上背负着哪些任务,(内核并不是只给一个应用程序服务),什么时候才能帮我们去创建或销毁线程,整个过程是不可控的。
用户态比内核态更高效,所以使用线程池更高效。
三、线程池的使用
Java标准库中,提供了现成的线程池,可以直接使用。
这里使用到了“工厂模式”这种设计模式。
像 newFixedThreadPoll()这样的方法,称为“工厂方法”,提供这个工厂方法的类(Executors),称为“工厂类”。 工厂方法一般都是普通的静态方法,使用工厂类的工厂方法,来代替构造方法创建对象。(相当于把new对象的代码,放到工厂方法里了,我们只需要调用工厂方法,就能直接构造出一个对象来)
工厂类Executors 提供的线程池有很多种:
ExecutorService pool1 = Executors.newFixedThreadPool(10); 构造出固定线程数的线程池 ExecutorService pool2 = Executors.newCachedThreadPool(); 线程数量是动态变化的(如果任务多了,就多搞几个线程;如果任务少了,就少搞几个线程) ExecutorService pool3 = Executors.newSingleThreadExecutor(); 线程池里只有一个线程 ScheduledExecutorService pool4 = Executors.newScheduledThreadPool(10); 类似于定时器,让任务延时执行。
我们用 Executors.newFixedThreadPool(10)来举例。
此处是通过工厂方法创建了一个10个线程的线程池对象(线程池里已经有10个线程了,这些线程都是前台线程),然后我们就可以随时安排这些线程去干活了。
通过线程池提供的 submit方法,可以给线程池提交若干个任务(往线程池的任务队列里放任务)
于是,线程池里的每个线程,就会自己从任务队列中取走一个任务去执行,执行完,再立即去取下一个任务去执行。
代码如下:
public static void main(String[] args) {
//构造了一个10个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
//通过submit,往线程池的任务队列里放100个任务
for (int i = 0; i < 100; i++) {
int n = i;
pool.submit(new Runnable() {
@Override
public void run() {
//往任务队列中放的的任务
System.out.println("hello "+n);
}
});
}
}
所以,使用Java线程的线程池时,只要通过submit往线程池的任务队列中指定任务就行啦,是不是非常方便呢。
四、实现一个线程池
实现固定数量线程的线程池
一个线程池里面至少要有2个大的部分
(1)阻塞队列:用来去保存任务
(2)若干个工作线程,每个线程的活:循环(从阻塞队列中取出任务,去执行)
提供一个submit方法,调用submit方法可以往任务队列中放任务。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool{
//任务队列
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//线程数
public MyThreadPool(int n){
//创建n个线程,每个线程都会不停从任务队列里拿任务去执行
for (int i = 0; i < n; i++) {
Thread t = new Thread(()->{
while(true){
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
//往任务队列里放任务
public void submit(Runnable runnable){
try {
queue.put(runnable);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public class ThreadDemo8 {
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 10; i++) {
int n = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello "+n);
}
});
}
}
}
五、认识 ThreadPoolExecutor类 中构造方法的参数
上面介绍的工厂类Executors提供的这些线程池,本质上都是通过包装 ThreadPoolExecutor来实现出来的。只是因为ThreadPoolExecutor这个线程池用起来有些麻烦,所以才提供了工厂类,让我们用起来比较简单。ThreadPoolExecutor用起来麻烦的原因主要是因为它提供的功能更强大。所以也需要我们对它的构造方法进行掌握。
ThreadPoolExecutor这个类也在 java.util.concurrent包底下,这个包里放的很多类都是和并发编程(多线程编程)密切相关的。
corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime 和 unit 描述了线程 可以闲着不干活 的最大时间
workQueue:线程池的任务队列
threadFactory:用于创建线程
handler:描述了线程池的“拒绝策略”
ThreadPoolExecutor 相当于把里面的线程分为2类:一类是公司的正式员工,一类是临时工。正式员工就相当于核心线程。
核心线程数就是正式员工的数目,最大线程数就是正式员工+临时工的数目
如果任务多,就可以多招一些临时工,多搞一些线程。但是一个程序的任务不一定始终都很多,任务少时,有些临时工就闲着了,就不需要这么多临时工了。就需要动态调节线程的数目,对现有的一些线程进行淘汰。
keepAliveTime 和 unit就描述了临时工 可以闲着不干活 的最大时间,unit是时间单位
workQueue,是线程池的任务队列,此处使用阻塞队列。如果队列为空,执行出队列操作就会阻塞,直到队列里又有活了(队列不为空)。如果队列满了,往队列里添加任务也会阻塞,阻塞到队列里的任务被线程拿走执行了(队列不满)
threadFactory,用于创建线程,线程池里是需要创建线程的
handler,描述了线程池的“拒绝策略”,是一个特殊的对象,描述了当线程任务队列满了时,如果继续添加任务会有啥样的行为。标准库提供了四个拒绝策略。
如果任务队列满了,第一种,直接抛异常;第二种 ,多出来的任务,谁加的,谁负责执行。第三种,丢弃最早的任务。第四种,丢弃最新的任务。
六、实际开发中,线程池的线程数,我们到底应该设置成多少合适呢?
不同的程序。特点不同,此时要设置的线程数也是不同的。
有两个极端情况:
第一种,CPU密集型。每个线程执行的任务都需要疯狂使用CPU(如:进行一系列算术运算),此时线程池的线程数,最多也不应该超过CPU核数。设置更多也没有,没有CPU给他用了。
第二种,IO密集型。每个线程要干的工作就是等待IO(如:读写硬盘,读写网卡,等待用户输入),此时这样的线程处于阻塞状态,不参与CPU的调度。这个时候你想搞多少线程都可以。
然而,这两种只是理想化的模型,真实的程序,往往一部分要用CPU,一部分要等待IO,具体几成工作量是用CPU,几成工作量是用IO,都是不确定的。所以线程池的线程数,应该设置成多少也是不确定的。
所以,确定线程数,一般通过测试/实验的方式去确定。