• 【多线程案例】Java实现线程池


    线程诞生的意义是因为进程的创建与销毁太重量了,也耗时,与进程相比,线程是更快了,但是如果进一步提高创建销毁的频率,线程的开销也不能忽略。

    两种典型的解决办法:第一种是使用协程(轻量级线程),相比于线程,把系统调度的过程给省略了。第二种就是要讲的线程池。

    池:池这个词,在计算机中是一种重要的思想,在很多地方都能用到。比如进程池,内存池,常量池,线程池等。

    什么是线程池

    线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;

    通俗的来讲,就是提前把线程创建好,放在池子里,后续用的时候直接从池子里来取,不必重新创建,这样创建线程的开销就被降低了。那么为啥从池子里取的效率比新创建的线程效率更高?因为从池子里取,这个动作是纯用户态的操作。而创建新的线程这个动作,则是需要用户态+内核态相互配合完成的操作。

    内核

    操作系统是由内核+配套的应用程序组成。内核是系统最核心的部分,创建线程操作就需要调用系统API,进入到内核中,按照内核态方式完成一系列操作。

    内核态和用户态

    如果一段程序,是在系统内核中执行,此时就被称为"内核态",如果不是,则称为"用户态"。

    内核态操作:是要给所有的进程提供服务的,当你要创建进程的时候,内核会帮我们做,但是在做的过程中难免也要做一些其他的事。过程不可控。

    用户态操作:过程可控。

    为什么使用线程池

    • 使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处。
    • 优化频繁创建销毁线程的操作。
    • 总的来说:虽然创建销毁线程比创建销毁进程更轻量,但是如果在频繁创建销毁线程的时候使用线程还是会比较低效。而线程池就是为了解决这个问题,如果某个线程被创建出来后不再使用了,线程池的做法并不是把它立即销毁,而是放到一个"池子"里,下次如果需要用到线程了,就会首先去池子里使用现成的,不必又再去创建一个线程。毕竟创建销毁线程都需要消耗时间,消耗资源。

    使用线程池有哪些优势

    1. 线程和任务分离,提升线程重用性;
    2. 控制线程并发数量,降低服务器压力,统一管理所有线程;
    3. 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;

    Java标准库中的线程池 ExecutorService

    第一种:

    注意:线程池对象不是直接new的,而是通过一个专门的方法,返回了一个线程池对象。这种方式我们称为工厂模式,是设计模式中的一中。

    代码中 Executors 称为工厂类,newCachedThreadPool() 称为工厂方法cache翻译为缓存。即用过之后不急着释放,以备随时使用。

    特点:

    • 这样构造出的线程池对象有一个基本的特点就是线程数目是动态适应的。随着往线程池里添加任务,这个线程池中的线程会根据需要自动被创建出来。创建出来后也不会急着释放,会在池子里保留一定的时间,以备随时再使用。

    第二种:

    特点:

    • 线程数在创建对象之初就被固定了。这里我就创建了4个。Fixed翻译为固定的

    第三种:

    特点:

    • 线程数只有固定的一个。single翻译为单例

    什么是工厂模式?

    是用工厂方法代替new操作的一种模式,是为了弥补构造方法的局限性。

    在我们学习Java语言中,一般都见过当创建实例对象时,有的就会提供多种构造方法(带参的,不带参的等等)供我们使用,这些方法显示是重载关系,要求是方法名相同,构造方法的话还必须跟类名一样,并且要求参数类型和个数不能相同。但对于下面这种情况。

    假设有各类,我们期望通过这个类,构造平面上的点。有两种方式。一种是利用直角坐标系,另一种是利用极坐标。

    class Point {
            public Point(double x,double y) {}

            public Point(double r,double a) {} 
    }

    显然这两方法没有构成重载,就会编译失败。而如果使用工厂模式,可以单独搞一个

    类,给这个类写一些静态方法,由这些静态方法负责构造出对象:

    class PointFactory {

            public static Point makePointByXY() {

                   Point p = new Point();

                    p.setX(x);

                    p.setY(y);

                    return p;

            }

            public static Point makePointByRA() {

                    ....

            }

    }

    调用:Point p = PointFactory.makePointByXY(12,16);

    上述这几个工厂方法生成的线程池,本质上都是对一个类的封装:ThreadPoolExecutor。相当于下面例子中的Point。

    ThreadPoolExecutor部分源码

    1. 构造方法:
    2. public ThreadPoolExecutor(int corePoolSize, //核心线程数量 相当于正式员工
    3. int maximumPoolSize,// 最大线程数 相当于正式员工+实习生
    4. long keepAliveTime, // 最大空闲时间
    5. TimeUnit unit, // 时间单位
    6. BlockingQueue workQueue, // 任务队列 阻塞队列
    7. ThreadFactory threadFactory, // 线程工厂 工厂模式的体现
    8. RejectedExecutionHandler handler // 饱和处理机制 拒绝策略
    9. )
    10. { ... }

    线程数目 (面试考点)

    上面源码中前两个参数的设置。

    • 核心线程数:相当于正式员工
    • 最大线程数:相当于正式员工+实习生

    拒绝策略(面试考点)

     拒绝策略指的是上面展示源码的最后一个参数 RejectedExecutionHandler handler 的设置。

    1. AbortPolicy:直接抛出异常
    2. CallerRunsPolicy:新添加的任务有心添加任务的线程执行
    3. DiscardOldestPloicy:丢弃任务队列中最老的队伍
    4. DiscardPolicy:丢弃当前新加的任务

    BlockingQueue(阻塞队列)

    上面源码中第五个参数 BlockingQueue workQueue 的设置。

    1. 若需要优先级,就可以设置成PrioritylockingQueue。

    1. 如果不需要优先级,并且任务数目是相对固定的,可以使用ArrayBlockingQueue。

    1. 如果不需要优先级,并且任务数目变动较大,可以使用LinkedBlockingQueue。

    ThreadPoolExecutor参数详解

    1. 我们可以通过下面的场景理解ThreadPoolExecutor中的各个参数;
    2. a客户(任务)去银行(线程池)办理业务,但银行刚开始营业,窗口服务员还未就位(相当于线程池中初始线程数量为0),
    3. 于是经理(线程池管理者)就安排1号工作人员(创建1号线程执行任务)接待a客户(创建线程);
    4. 在a客户业务还没办完时,b客户(任务)又来了,于是经理(线程池管理者)就安排2号工作人员(创建2号线程执行任务)接待b客户(又创建了一个新的线程);假设该银行总共就2个窗口(核心线程数量是2);
    5. 紧接着在a,b客户都没有结束的情况下c客户来了,于是经理(线程池管理者)就安排c客户先坐到银行大厅的座位上(空位相当于是任务队列)等候,
    6. 并告知他: 如果12号工作人员空出,c就可以前去办理业务;
    7. 此时d客户又到了银行,(工作人员都在忙,大厅座位也满了)于是经理赶紧安排临时工(新创建的线程)在大堂站着,手持pad设备给d客户办理业务;
    8. 假如前面的业务都没有结束的时候e客户又来了,此时正式工作人员都上了,临时工也上了,座位也满了(临时工加正式员工的总数量就是最大线程数),
    9. 于是经理只能按《超出银行最大接待能力处理办法》(饱和处理机制)拒接接待e客户;
    10. 最后,进来办业务的人少了,大厅的临时工空闲时间也超过了1个小时(最大空闲时间),经理就会让这部分空闲的员工人下班.(销毁线程)
    11. 但是为了保证银行银行正常工作(有一个allowCoreThreadTimeout变量控制是否允许销毁核心线程,默认false),即使正式工闲着,也不得提前下班,所以12号工作人员继续待着(池内保持核心线程数量);

    设置线程数目 (面试考点)

    使用线程池,若要设置其数目,设置多少合适?

     其实没有一个具体的数字,要根据场景确定。总结起来就是两种情况。

    一个线程执行的代码,主要有两类(设CPU核心数 (逻辑核心数) 为N):

    1.CPU密集型:

    • 代码里主要的逻辑是在进行算术运算或逻辑运算。则线程池的数量不应该超过N。如果超过N,也无法提高效率了,因为CPU吃满了,此时更多的线程反而会增加调度的开销。

    2.IO密集型:

    • 代码里主要进行的是IO操作,并不吃CPU,就可以超过N。可以通过调度的方式并发执行。

    而代码不同,线程池的线程数目设置就不同。由于无法知道一个代码,具体多少内容是CPU密集型,多少内容是IO密集型。

    正确的做法:

    • 使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程数目,看哪些情况下,最符合你的要求。

    代码实现 MyThreadPool类

    1. import java.util.concurrent.ArrayBlockingQueue;
    2. import java.util.concurrent.BlockingQueue;
    3. //线程池 ThreadPool
    4. class MyThreadPool {
    5. //任务队列 阻塞队列实现
    6. private BlockingQueue queue = new ArrayBlockingQueue<>(5); // 队列的容量5 队列中存的是任务
    7. //将任务添加到队列中
    8. public void submit(Runnable runnable) throws InterruptedException {
    9. //拒绝策略 等待阻塞 和 那四种有所不同 但这也是一种策略
    10. queue.put(runnable);
    11. }
    12. //构造方法
    13. public MyThreadPool(int n) {
    14. //n 即创建出n个线程
    15. for(int i=0;i
    16. Thread t = new Thread(() -> {
    17. try {
    18. //获取队列头任务
    19. Runnable runnable = queue.take();
    20. //执行队头任务
    21. runnable.run();
    22. } catch (InterruptedException e) {
    23. e.printStackTrace();
    24. }
    25. });
    26. //启动线程
    27. t.start();
    28. }
    29. }
    30. }
    31. //测试
    32. public class test {
    33. public static void main(String[] args) throws InterruptedException {
    34. MyThreadPool myThreadPool = new MyThreadPool(10);
    35. for (int i = 0; i < 1000; i++) {
    36. int tmp = i; //内部类访问外部类变量的变量捕获 该变量得是final或是有效的final
    37. myThreadPool.submit(new Runnable() {
    38. @Override
    39. public void run() {
    40. System.out.println("执行任务: "+tmp);
    41. }
    42. });
    43. }
    44. }
    45. }

    线程池的缺点

    1.非核心线程的创建时机

                1.1) 核心线程的数量是 corePoolSize 的值,非核心线程的数量是 maxinumPoolSize - corePoolSize ;

                1.2) 非核心线程创建的触发时机是:当前线程池中核心线程已满,且没有空闲的线程,还有任务等待队列已满,

    满足上面的所有条件,才会去创建线程去执行新提交的任务

               1.3) 如果线程池中的线程数量达到 maxinumPoolSize 的值,此时还有任务进来,就会执行拒绝策略,抛弃任务或者其他

    如果拒绝策略是抛弃任务的话,有一种场景,就会造成大量任务的丢弃,就是瞬时冲高的情况下。

    2.排队任务调度策略

              当线程池中核心线程数量已达标,且没有空闲线的情况下,在产生的任务,会加入到等待队列中去,这样一直持续下去,

    等到等待队列已满,在来的任务,会创建非核心线程去执行新提交的任务,那么就产生一种结果,在等待队列中的任务是先提

    交的任务,反而没有在此时提交的任务先执行。

    任务的执行顺序和任务的提交顺序不一致,如果业务需求的任务是有先后依赖关系的,就会降低线程的调度效率

  • 相关阅读:
    SpringBoot整合oceanbase,实现oracle无缝切换到oceanbase
    2023 年 Web 应用程序开发最佳技术堆栈
    JVM简介
    grafana对指标进行组合计算
    程序员-数据库选型经验
    精华推荐 | 【深入浅出RocketMQ原理及实战】「底层原理挖掘系列」透彻剖析贯穿RocketMQ的存储系统的实现原理和持久化机制
    SpringBoot + MyBatis 结合 MVC框架设计 第2关:使用SpringBoot + MyBatis实现一个最简单的注册功能
    Java使用JJWT令牌
    【VS2017】MIDL : CreateFile() error : 2
    王怀民院士:开源创新必将成为中国迈向世界科技强国的战略机遇
  • 原文地址:https://blog.csdn.net/m0_73381672/article/details/133744579