编程(41) ----------线程池

线程池是存放线程的池,用于提高资源利用率,减少系统内核对线程创建和销毁的开销。在Java中,可以通过ExecutorService创建线程池,例如newFixedThreadPool。线程池的参数如核心线程数、最大线程数、存活时间和拒绝策略等需根据具体任务进行调整。文章还提到了变量捕获的概念,以及线程池可能导致的任务执行顺序不确定性。
摘要由CSDN通过智能技术生成

本篇主要提及线程池的相关内容.

依旧是从最基础的含义开始. 什么是线程池? 在计算机中池的是一个很大的概念, 分为很多种. 但无论是什么池, 其核心都是存取相关数据.

线程池也不例外, 即存放线程的池. 其存在意义与线程异曲同工. 线程产生并使用是因为进程太"重"了. 这里所说的重是其消耗的各个资源过多, 不太利于计算机的快速响应运行, 也就是说线程就可以说成是"轻量级进程". 随着时间的推移, 线程不再能满足更快速的编程, 因此又在线程的基础上有了线程池. 

其大概原理就是, 先创建若干个线程, 并放入线程池中. 随去随用, 用完放回. 这相比于单独一个个创建或者销毁线程所耗费的资源来说, 更为轻量. 为什么? 因为线程的生死大权是掌握在系统内核手上的, 而线程池获取或放回全由用户自身决定. 举个简单的例子:  

                                                                            

在超市一般都会用一个散货区, 这里的东西都是论斤卖. 也就是说, 购买流程上客户拿袋子, 装上相应所需数量的要购买的散货, 到指定地点去打包封上贴条, 最后再去收银台结账. 

假设打包这个操作是"内核态", 也就是必须要由内核操作来完成. 因此, 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晚上八点左右 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值