多线程(六)
一,线程池
池(pool)本质是一种“提高效率”的机制
类比于常量池:就是把一些要用到的常量提前准备好,后面随时需要,随时就取
线程池:把线程提前创建好,放到池子中,随时需要用线程,随时来取
操作系统创建线程,这个过程涉及到“用户态和内核态”之间的切换
如果提前创建好线程,直接从池子里取,“从池子取”这个动作,纯用户态的操作,不涉及到用户态和内核态的切换
操作系统 = 内核 + 配套的应用程序
内核是操作系统最核心的功能集合(对于硬件的管理,对于软件的管理....)
内核中进行的操作,更底层,需要更大的权限,也涉及到更高的风险,如果普通的应用程序,出现bug,最多进程崩溃,不会影响其他进程,如果是内核中的逻辑出bug,整个系统就蓝屏了
现代的CPU有权限划分
如果你的指令是应用程序的指令,cpu以更低权限模式来运行
如果你的指令是内核的指令,cpu以更高权限的模式来运行
内核态的操作是不可控的
使用线程池,线程池是纯用户态代码实现的,加入线程池/从池子中取线程,都是纯用户态操作----------------------效率更高
调用操作系统的API创建新线程,涉及到用户态和内核态的切换---------效率更低
1.1 线程池是什么
虽然创建线程/销毁线程的开销
想象这么⼀个场景:
在学校附近新开了⼀家快递店,⽼板很精明,想到⼀个与众不同的办法来经营。店⾥没有雇⼈,⽽是每次有业务来了,就现场找⼀名同学过来把快递送了,然后解雇同学。这个类⽐我们平时来⼀个任务,起⼀个线程进⾏处理的模式。
很快⽼板发现问题来了,每次招聘+解雇同学的成本还是⾮常⾼的。⽼板还是很善于变通的,知道 了为什么⼤家都要雇⼈了,所以指定了⼀个指标,公司业务⼈员会扩张到3个⼈,但还是随着业务 逐步雇⼈。于是再有业务来了,⽼板就看,如果现在公司还没3个⼈,就雇⼀个⼈去送快递,否则 只是把业务放到⼀个本本上,等着3个快递⼈员空闲的时候去处理。这个就是我们要带出的线程池 的模式。
线程池最大的好处就是减少每次启动、销毁线程的损耗
1.2 Java标准库中的线程池
1.2.1 线程池的构造方法

针对最后一种构造方法:

其中:
为 核心线程数 最大线程数
核心线程数:
Java标准库提供的线程池,里面的线程个数,不是固定的,而是动态变化的
任务多的时候,需要有更多的线程,提高总体的效率
任务少的时候,释放不必要的线程,节省系统开销
核心线程:不会随便释放(除非整个线程池释放)
非核心线程:在任务少的时候,被自动释放
最大线程数:
核心线程和非核心线程总数
其中:
保活时间 时间单位
保活时间:
非核心线程不被释放的最大空闲时间
其中:
线程池用到的阻塞队列
这个队列用来存放要让线程池执行的任务
一个线程池,可以处理很多任务,需要有数据结构保存多个任务,submit的功能就是把要执行的任务(Runnable对象)放到队列中
线程池内部的线程,从队列中取任务,并执行
这个队列是用户指定的,不是封装好的,给用户更多的操作空间
用户可以限制,队列的最大长度,设置队列的实现的底层结构,也可以支持优先级队列(带有优先级的任务)
其中:
线程工厂(工厂设计模式)
//表示平面上的一个点
class Point {
通过横纵坐标来构造点
public Point(double x, double y) {
// ...
}
// 通过极坐标构造点
public Point(double r, double a) {
}
}
在这个代码中就报错了,因为没有构成重载,无法通过编译
构造方法,要想提供多个版本,需要通过重载语法来支持,重载是有限制的,要求参数个数/参数类型不同
所以此时就可以利用工厂设计模式,工程设计模式不使用构造方法进行初始化对象,而是使用专门的“工厂方法”来进行构造,工厂方法就是普通的方法,没有“名字”“重载”的限制
通过工厂方法封装了构造方法
// 工厂类
class PointBuilder {
// 工厂方法
public static Point buildPointByXY(double x, double y) {
Point p = new Point();
p.setX(x);
p.setY(y);
return p;
}
public static Point buildPointByRA(double r, double a) {
Point p = new Point();
p.setR(r);
p.setA(a);
return p;
}
}
所以 这个ThreadFactory就是一个工厂类,这个类里提供了工厂方法,通过工厂方法来创建线程

实际使用的时候需要实现这个接口,实现这个抽象方法,自己实现创建线程对象的操作
而这样封装起来是因为:通过线程工厂,创建线程池内部的线程,把创建线程的操作,通过工厂封装提供给用户,就是为了让用户制定一些“自定义的逻辑”
比如想要让线程池里面的线程都是后台线程
其中:
拒绝执行操作----拒绝策略
线程池有任务队列(阻塞队列)
当队列满了应该怎么办?
阻塞不合适,因为对于线程池来说,希望每个任务都能够尽快被执行掉,但是如果带有阻塞了,此时这个任务啥时候被执行,就可能遥遥无期
希望通过“拒绝策略”来不要堵塞,而是通过其他的方法来进行处理


如果满了还添加会直接抛出异常

调用者执行
哪个线程调用的submit方法,哪个线程就负责执行这个任务

丢弃队列中最老的任务,腾出位置交给新任务

丢弃队列中最新的任务,腾出位置交给新任务
------------------------------------------------------------------------------------------------------
1.2.2 标准库中的线程池Executors
标准库提供了针对ThreadPoolExecutor封装的工厂类Executors来简化编程
• 使⽤Executors.newFixedThreadPool(10)能创建出固定包含10个线程的线程池
• 返回值类型为ExecutorService
• 通过ExecutorService.submit可以注册⼀个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
Executors 创建线程池的⼏种⽅式
• newFixedThreadPool:创建固定线程数的线程池
• newCachedThreadPool:创建线程数⽬动态增⻓的线程池.
• newSingleThreadExecutor: 创建只包含单个线程的线程池.
• newScheduledThreadPool:设定延迟时间后执⾏命令,或者定期执⾏命令.是进阶版的Timer. Executors 本质上是ThreadPoolExecutor类的封装.
1.2.3 实现线程池
• 核⼼操作为submit,将任务加⼊线程池中
• 使⽤Worker类描述⼀个⼯作线程.使⽤Runnable描述⼀个任务.
• 使⽤⼀个BlockingQueue组织所有的任务
• 每个worker线程要做的事情:不停的从BlockingQueue中取任务并执⾏.
• 指定⼀下线程池中的最⼤线程数maxWorkerCount;当当前线程数超过这个最⼤值时,就不再新增 线程了.
class MyThreadPool {
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//通过这个⽅法来把任务添加到线程池中
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
// n表⽰线程池⾥有⼏个线程.
//创建了⼀个固定数量的线程池
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
while (true) {
try {
//取出任务,并执⾏~~
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}
//线程池
public class Demo25 {
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(Thread.currentThread().getName() + " hello");
}
});
}
}
}
二,保证线程安全的思路
1.使⽤没有共享资源的模型
2.适⽤共享资源只读,不写的模型
a.不需要写共享资源的模型 b.使用不可变对象
3.直⾯线程安全(重点)
a.保证原子性 b.保证顺序性 c.保证可见性
三,对比线程和进程
3.1 线程的优点
1.创建⼀个新线程的代价要⽐创建⼀个新进程⼩得多
2.与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多
3.线程占⽤的资源要⽐进程少很多
4. 能充分利⽤多处理器的可并⾏数量
5. 在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务
6. 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现
7. I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
3.2 进程与线程的区别
1. 进程是系统进⾏资源分配和调度的⼀个独⽴单位,线程是程序执⾏的最⼩单位。
2. 进程有⾃⼰的内存地址空间,线程只独享指令流执⾏的必要资源,如寄存器和栈。
3. 由于同⼀进程的各线程间共享内存和⽂件资源,可以不通过内核进⾏直接通信。
4. 线程的创建、切换及终⽌效率更⾼
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)