本篇主要提及线程池的相关内容.
依旧是从最基础的含义开始. 什么是线程池? 在计算机中池的是一个很大的概念, 分为很多种. 但无论是什么池, 其核心都是存取相关数据.
线程池也不例外, 即存放线程的池. 其存在意义与线程异曲同工. 线程产生并使用是因为进程太"重"了. 这里所说的重是其消耗的各个资源过多, 不太利于计算机的快速响应运行, 也就是说线程就可以说成是"轻量级进程". 随着时间的推移, 线程不再能满足更快速的编程, 因此又在线程的基础上有了线程池.
其大概原理就是, 先创建若干个线程, 并放入线程池中. 随去随用, 用完放回. 这相比于单独一个个创建或者销毁线程所耗费的资源来说, 更为轻量. 为什么? 因为线程的生死大权是掌握在系统内核手上的, 而线程池获取或放回全由用户自身决定. 举个简单的例子:
在超市一般都会用一个散货区, 这里的东西都是论斤卖. 也就是说, 购买流程上客户拿袋子, 装上相应所需数量的要购买的散货, 到指定地点去打包封上贴条, 最后再去收银台结账.
假设打包这个操作是"内核态", 也就是必须要由内核操作来完成. 因此, ABC都必须将袋子交给服务员进行打包. D是属于线程池中的线程, 是属于"用户态", 所执行的是程序员自己所写的代码, 想怎么做全由自己决定. D可以选择将袋子给服务员, 但是服务员不一定会马上帮他打包, 因为已经有客户在进行等待. 同样的道理, 一个系统内核也不是只处理一个线程. 不可能来一个处理一个.
因此, 何必等待. 直接由自身控制自身打包. 能自由控制, 也能提高效率.
在Java中也能创建线程池:
public class Main {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 50; i++) {
int n = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(n);
}
});
}
}
}
这是最简单的线程池. 但这段代码中需要注意一些问题.
首先, 在创建线程池的时候, 可以注意到相比于以往创建线程时new一个对象而言. 线程池的创建不再是单独new关键字, 而是转为了方法的一部分. 这相当于在调用该方法的时候, 也new了对象只不过将其隐藏在方法中了. 这种方法, 以及这种方法所被包含的类, 可以被称为工厂模式. 即使用普通方法来代替构造方法, 进行new对象的操作.
为何这么做? 假设有一个坐标系. 要在坐标系上构造一个点, 若是使用笛卡尔坐标系, 就得知道其x轴的值以及y轴的值. 若用极坐标表示, 就得知道其斜边以及斜边和x轴夹角的值,使用三角函数得出结论.
那么现在问题来了. 假设有一个构造方法public point (double a, double). 其内部就是实现的以两个参数实现返回一个点. 请问现在这个方法内部应该调用什么? 是笛卡尔坐标的算法还是极坐标的算法? 没法确定. 因为二者所需参数相同, 返回值也相同. 而多个构造方法是通过重载来实现的. 重载有要求参数不同.
在这种特殊情况下, 无法使用构造方法来实现. 只能考虑工厂模式在方法内部调用对应的方法实现对应的算法.
其次, 在run方法中. 注意在打印时并非是直接打印 i , 而是另建一个n再打印. 如果直接打印 i, 编译器是会报错的.
其原因在于, 变量捕获. 首先要明确, 在这个代码中, 是有可能存在主线程for循环已经走完, 而run方法还未执行完的情况, 因为是多线程. 那在这种情况下, 就有可能 i 还来不及打印, 循环走完 i, 就被销毁了.
为避免这种情况, run方法会提前将 i 复制一份, 并在执行时创建使用, 这就是变量捕获. 但是, 变量捕获是有条件的, 在JDK1.8 以前, 条件极为苛刻. 1.8以后放松条件, 只要该变量在代码中没有被修改, 即可捕获. 因此在该代码中, i 明显是变化的, 不能进行捕获. 将其赋值n后, 不再变化, 就可以使用了.
最后, 若执行这个代码, 会发现打印出的数字是乱序的. 因为十个线程抢占式执行, 完全不会考虑顺序问题. 若代码执行完, 程序也不会自动结束, 需要手动停止. 这说明, 线程池所创建的线程全是前台线程.
再来简单看一下实现线程池的类的构造方法, 以第四个为例.
corePoolSize: 核心线程数.
maximumPoolSize: 最大线程数
可以这么理解, 核心线程数就好比是一个公司的正式员工. 而还有一类线程就相当于是临时工. 二者结合起来就是最大线程数.
keepAliveTime: 存活时间
unit: 这是上述的时间单位
这两个用于描述非核心线程的最长存在时间, 超时即销毁.
也就是说, 当任务较多的时候. 除核心线程以外还会增加一部分临时线程进行对任务的执行. 当任务较少的时候, 就会进行销除. 但是无论怎样, 都不可能超过最大线程数. 那问题来了, 这个线程数设定多少合适? 这个其实并没有一个准确的定论. 依据不同程序特点的不同, 那么它的设置也就不同, 需要进行相应的测试才能给个大概的线程数设定.
workQueue: 线程池任务队列
要明确这个队列肯定是阻塞队列. 因为线程数量是一定的. 每个线程都从中take, 若没有任务放入队列中, 就阻塞.
threadFactory: 线程工厂, 用于创建线程.
handler: 拒绝策略, 描述若线程池任务队列满了, 继续强加任务会发生什么. 以下是四个标准库中提供的拒绝策略:
-----------------------------------最后编辑于2023.6.14晚上八点左右