目录
一、线程池
线程池就是提前创建好了一批线程,放到一个池子中,当有任务来的时候,就从池子中去取出一个线程(就不用从系统这边申请了),去执行这个任务,执行完后又放回到池子中。
- 进程比较 “重”,频繁的创建和销毁开销很大,因此有了 进程池 和 线程 。
- 线程是 “轻” 了,但是更频繁的创建销毁还是会有很大的开销。因此有了 线程池 和 协程 。
使用线程池的话,就没有频繁的创建销毁了,速度就快了 。
1、 减小开销的内部原因
主要原因就是 用户态 和 内核态 的速度。
上图是计算机的大致层次结构。我们平常写的代码就是在最上层 应用程序 这层来运行的,而这里的代码都被称为: “用户态” 运行的代码。 但是有写代码需要调用操作系统的 API,从而会在内核中执行。(例如调用 System.out.println,本质上要经过 write 系统调用,进入到内核中,内核会执行很多逻辑,最后控制显示器输出字符)在内核中运行的代码,就被称为: “内核态” 运行的代码。
创建线程,本身就需要内核的支持。(本质是在内核中创一个 PCB ,加到链表里)因此调用的 Thread.start 也是要进入内核态来运行的。
而把创建好的线程放在池子里,因为这个池子就是用用户态实现的,因此把线程放到池子 / 从池子中取出线程,都是用纯粹的用户态代码实现的,不涉及内核态。
我们认为,纯用户态的操作,效率要比经过内核态处理的操作,效率更高。因为当代码进入内核态后,由于内核态要做的操作有很多,我们不知道什么时候才能执行我们这个代码,因此具有不可控性。所以说纯用户态的操作效率更高。
2、 各个参数的含义
- 标准库的线程池: ThreadPoolExecuter
其内部的参数:
- int corePoolSize :核心线程数 (正式员工的数量)
- int maximumPoolSize :最大线程数(正式员工 + 临时员工 的数量)
- long keepAliveTime :线程的空闲时间(允许临时员工摸鱼的时间)
- TimeUnit unit :时间单位(s、ms、us....)
- BlockingQueue<Runnable> workQueue :任务队列(线程池提供一个 submit 方法,让我们把任务注册到线程池中,加入到这个任务队列)
- ThreadFactory threadFactory :线程工厂(线程是怎么创建出来的,一般使用默认的)
- RejectedExecutionHandler handler :拒绝策略(当任务队列满了的时候怎么办?直接丢弃最老的任务 / 阻塞等待 / 直接忽略最新的任务 ....)
类似在一个公司中,既有正式员工也有临时员工。正式员工不能随便开除,而临时员工有任务的时候就招进来,没有任务了就解雇。(任务多了就创建一些临时线程,任务执行完了就销毁)但是临时工也不能一干完活就直接解雇,公司要观察这一段时间的工作量,发现最近一段时间没有较多任务了,才会裁掉。(线程的空闲时间)。
当公司内还没有临时工的时候,工作太多了,就会将很多工作记录下来。(任务存储在任务队列中) 而当任务多到 本子记不下了的时候,就会执行拒绝策略。
3、 ***线程池的工作流程 ***
(1)最开始的时候,线程池是一个空的。(公司内一个员工也没有)
(2)随着任务的提交,开始创建线程
① if (当前线程数 < corePoolSize ) , 就创建线程 (正式员工)
② if (当前线程数 == corePoolSize ),就把任务添加到工作队列中 (先让正式员工紧着干)
③ 队列满了, if (当前线程数 < maxmumPoolSize ),创建线程(紧着干也干不完了,招临时员工)
④ 队列满了, if (当前线程数 == maxmumPoolSize ),执行拒绝策略(招临时员工后,也干不完,就拒绝,不接任务了)
(3)随着任务的执行,剩余的任务逐渐减少,逐渐有了空闲的线程:
if (空闲时间 > keepAliveTime , && 当前线程数 > corePoolSize) ,销毁线程
直到 当前线程数 == corePoolSize 。(公司任务少了,不需要临时员工了)
4、 ***如何设置参数 ***
*** 如何设置参数?
如果使用线程池的话,多少线程数合适?
要经过性能测试的方式找到合适的值。
通过不同的线程池的线程数,来观察 程序处理任务时的速度,以及 程序持有的CPU 的占用率。
- 当线程数多了,整体的速度是会变快,但是 CPU 的占用率也会变高;
- 当线程数少了,整体的速度是会变慢,但是 CPU 的占用率也会下降。
CPU 的占用率不能太高,因为我们需要留有一定的冗余,来处理突发情况。以服务器为例,如果本身就已经比 CPU 快占完了,这是突然来了一大波请求,可能服务器处理不过来就崩溃了。
因此需要找到一个让 程序速度(接口响应时间尽可能的快)能接受,并且 CPU 占用也合理的 平衡点。
5、 使用标准库中的线程池—— Executors 类
标准库中还提供了一个 简化版 的线程池 —— Executors 。它是针对 ThreadPoolExecuter 进行了封装,并且提供了一些默认参数。
其构造函数有返回值,返回值是 ExecutorService。
然后通过 ExecutorService 中的 submit 方法,就可以注册一个任务到线程池中。
(1)创建一个 固定线程数 的线程池,参数指定了线程的个数
ExecutorService pool = Executors.newFixedThreadPool(10);
(2)创建一个 自动扩容 的线程池,会根据任务量来 自动扩容
ExecutorService pool2 = Executors.newCachedThreadPool();
(3)创建一个 只有1个线程 的线程池
ExecutorService pool3 = Executors.newSingleThreadExecutor();
(4)创建一个 给带有定时器功能 的 线程数(类似 Timer,Timer中只有一个线程在执行,当任务过多时,可能就需要多个线程了)
ExecutorService pool4 = Executors.newScheduledThreadPool();
使用:
public class Demo1 {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
6、 自己实现一个 线程池
大致思路:
- 1. 描述任务(使用Runnable类)
- 2. 组织任务(使用 BlockingQueue)
- 3. 描述工作线程
- 4. 组织线程
- 5. 实现往线程中添加任务(实现 submit 方法)
我们要实现的线程池:一创建一个线程池,就有线程等待任务注册了。这里我们实现传入指定线程数的构造方法来创建线程池。
因此,自己实现的线程池类中,其构造方法要进行线程的创建,以及有一个方法能将任务注册到 任务队列 中,以便让线程来执行。
而具体的线程类,可以通过内部类来实现,要求这个线程中的 run 方法可以获取到 任务队列 中的任务,并执行。
具体代码:
class MyThreadPool {
//1. 描述一个任务 —— Runnable,不需要额外创建类了
//2. 组织任务 —— 阻塞队列
private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
//3. 描述线程 —— 线程的任务是到 任务队列 中 取出任务,并执行
//使用内部类来进行描述,继承自 Thread 即可
private class Worker extends Thread {
private BlockingQueue<Runnable> queue = null;
//创建线程对象时就开执行 run 方法
public Worker(BlockingQueue<Runnable> queue) {
this.queue = queue;
}
//run 方法
public void run() {
//这里需要能取到上面的 任务队列
//为了能取到外面的 任务队列,在这个类中定义一个队列的属性,并且在构造方法中传入外面任务队列,再赋给内部的队列即可
while(true) {
//循环去获取任务队列中的任务
//如果任务队列为空,就阻塞;如果不为空,就执行
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//4. 组织线程 —— List
private List<Thread> workers = new ArrayList<>();
//5.构造方法,不断创建线程
public MyThreadPool(int n) {
//创建若干个线程,放入上面的数组中
for (int i = 0; i < n; i++) {
Worker worker = new Worker(queue);
//因为继承了 Thread 方法,所以可以直接使用 start 方法创建启动线程
worker.start();
workers.add(worker);
}
}
//6. submit 方法,让程序员能够将任务放入线程池(任务队列)
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(10);
//for 循环 ,注册 100 个任务,10 个线程执行这 100 个任务。
for (int i = 0; i < 100; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
}