• 【JavaEE初阶】多线程 _ 基础篇 _ 线程池(案例四)


    ☕导航小助手☕

        🍚写在前面

             🍜一、线程池概述

             🍔二、线程池的使用方式

                   🍤🍤2.1 Java标准库中 线程池的使用

                   🥩🥩2.2 自己动手来模拟实现一个线程池


    写在前面

    本篇文章将介绍多线程案例 —— 线程池,这也是一个非常有用的案例~

    在之前已经介绍过,进程本身已经能做到并发编程,但是我们仍然创建了线程,是因为进程太重量了,创建和销毁的成本都比较高(需要申请释放资源)~

    线程,就是对上述问题的优化(共用同一组系统资源)~

    虽然如此,但是在更频繁创建释放的情况下,线程也不一定能够扛得住~

    因此,还需要去做进一步的优化,此时 可以有两种优化方式:

    1. 线程池
    2. 协程(又称 纤程,可以理解为 轻量级线程)

    关于 协程 的问题,现在暂时不给予介绍,本篇博客 主要介绍的是 线程池~

    那么,正文开始 ......

    一、线程池概述

    线程池和字符串常量池一样,都是为了提高程序的效率~

    其解决问题的思路,就是把线程创建好了以后,放到池子里,当我们需要使用线程的时候,就可以直接从池子里取,而不是通过系统来创建;当线程用完了,也是还到池子里,而不是通过系统来销毁线程~

    因此,上述操作 又能够进一步提高效率了~


    此时,就出现了一个关键问题:为什么把线程放到池子里,从池子里取线程就要比从系统这里创建线程 更高效呢?

    原因是 从池子里去取 是纯用户态操作,通过系统来创建 涉及到内核态操作~

    通常认为,牵扯到内核态的操作,就要比纯用户态的操作更低效!!!

    内核态:操作系统内核执行的工作,如前面所介绍的 线程、进程、PCB 它们的一些相关的管理和调度,都是由 系统内核 来负责的~

    当把任务交给内核态的时候,内核态不仅仅去完成交给它的工作,大概率还会伴随着其他的工作(内核态不会只为一个任务服务,还有其他的某些任务);而将工作交给用户态时,用户态仅仅完成交给它的工作~

    所以说,内核态要做的事情多了(你交给它的任务的确可以干完,但是不确定是不是可以立即马上干你交给它的任务);但是,用户态可以立即马上 去执行你交给它的任务~

    因此,用户态更为高效~ 

    二、线程池的使用方式

    2.1 Java标准库中 线程池的使用

    这样就创建了一个线程池对象,创建了一个 固定线程个数的线程池(此处的固定数量是 10个)~

    Executors 是一个类,newFixedThreadPool() 是这个类的静态方法,可以借助这个静态方法 来创建实例,像这样的方法,称为 "工厂方法",对应的设计模式,就叫做 "工厂模式"~ 

    通常情况下,创建对象 是借助 new,调用构造方法 来实现的~

    但是,C++ / Java 里面的构造方法,有诸多限制,在很多时候不方便使用~

    因此就需要给构造方法 再包装一层,外面起到包装作用的方法 就是工厂方法~ 

    构造方法的限制,在于 当前构造方法的名字 必须是和类名一样~

    要想实现不同版本的构造,就需要重载构造方法,但是 重载构造方法 又要求参数类型和个数不同~ 


    现在来列举一个关于 "工厂模式" 的例子:

    1. //创建一个表示点的例子
    2. class Point {
    3. //通过 横、纵坐标的方式
    4. public Point(double x,double y) {
    5. }
    6. //通过 极坐标的方式
    7. public Point(double r,double a) {
    8. }
    9. }
    10. //当直接构造对象的时候,就可以通过下面的来表示一个点:
    11. Point p = Point.makePointByXY(30,60);

    很明显,上面的代码不可以编译运行,无法构成重载,编译错误!!!

    为了解决上述问题,就可以使用 工厂模式:

    1. public static Point makePointByXY(double x,double y) {
    2. Ponit p = new Point();
    3. p.setX(x);
    4. p.setY(y);
    5. return p;
    6. }
    7. public static Point makePointByRA(double r,double a) {
    8. Point p = new Point();
    9. p.serR(r);
    10. p.setA(a);
    11. return p;
    12. }

    可以通过 上面的两种方法,来完成构造一个点的坐标~


    当然,Executors还可以创建其他方式的线程池,这些不同的 "工厂方法" 其实是对 ThreadPoolExextor线程池原始类 的构造方法的包装(ThreadPoolExextor自身的构造方法太麻烦了,针对 ThreadPoolExextor这个类进行了 new,并且传入不同风格的参数,来达到构造不同种类线程池的目标):

     其中,比较典型的两个线程池分别是:

    1. //固定个数的线程池
    2. Executors.newFixedThreadPool(10);
    3. //线程数量动态增加的线程池
    4. Executors.newCachedThreadPool();

    可以根据不同的需要,来选择不同的线程池~

    1. package thread;
    2. import java.util.concurrent.ExecutorService;
    3. import java.util.concurrent.Executors;
    4. public class Demo25 {
    5. public static void main(String[] args) {
    6. //创建线程池对象
    7. ExecutorService threadPool = Executors.newFixedThreadPool(10);
    8. //安排任务
    9. //并且把任务加到线程池里面去,由线程池里面的线程负责 执行其中的任务~
    10. threadPool.submit(new Runnable() {
    11. @Override
    12. public void run() {
    13. System.out.println("hello");
    14. }
    15. });
    16. }
    17. }

    运行结果:

    可以看见,程序一运行,"hello"就被打印出来了~

    同时和定时器类似,线程池内部有一些线程阻止了程序的退出,所以需要手动退出~ 


    线程池存在的目的,就是让程序员不必创建新的线程,直接使用已有的线程完成想要进行的工作即可~

    需要注意的是,虽然上面的线程池只有 10 个线程,但是并不是说 线程只执行 10 个任务~

    比如说,有一个餐馆里面需要洗客人吃饭过后 剩下的盘子~

    服务员会把用过的盘子收起来,放到一个非常大的盆里,需要洗碗工来洗盘子~

    虽然说,每个洗碗工每一次只能洗一个盘子,但是 这并不是说,每一个洗碗工只能洗一个盘子,他们洗完一个再洗下一个~

    当然,如果有 10个线程,给6个任务,那么也不一定10个线程都在工作(可能情况在 1~10 个之间)~

    10个洗碗工, 6个碗~

    那么 可能1个洗碗工洗1个,有4个在摸鱼;可能有一个洗碗工洗两个 ...... 

    线程之间的调度 是充满随机性的,但是 在更大的数据量级下,各个线程之间的工作是比较均衡的(相差的也不会太多)~ 

    2.2 自己动手来模拟实现一个线程池

    1. package thread;
    2. import java.util.concurrent.BlockingDeque;
    3. import java.util.concurrent.LinkedBlockingDeque;
    4. //自己写的线程池类
    5. //简单实现成,固定 10 个线程的线程池
    6. class MyThreadPool {
    7. //核心操作,往线程池里插入任务
    8. //由于插入操作 一下就可以插入很多任务,那么就需要把当前尚未执行的任务都保存起来
    9. //使用阻塞队列来保存
    10. //这个队列就是 "任务队列",把当前线程池要完成的任务都放到这个队列中,
    11. //再由线程池内部的工作线程负责完成它们
    12. private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<> ();
    13. public void submit(Runnable runnable) {
    14. //提交任务
    15. try {
    16. queue.put(runnable);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. }
    21. public MyThreadPool(int n) {
    22. //n 设定线程池里面有几个线程
    23. //构造方法中,就需要创建一些线程,让这些线程负责完成上述执行任务的工作
    24. for (int i = 0; i < n; i++) {
    25. Thread t = new Thread(() -> {
    26. //当前线程是否已经中断(中断:不执行 未中断:继续执行)
    27. while(!Thread.currentThread().isInterrupted()) {
    28. try {
    29. //把任务给取出来
    30. Runnable runnable = queue.take();
    31. //取出一个任务就执行一个任务
    32. runnable.run();
    33. } catch (InterruptedException e) {
    34. e.printStackTrace();
    35. break;
    36. }
    37. }
    38. });
    39. t.start();
    40. }
    41. }
    42. }
    43. public class Demo26 {
    44. //小小测试一下
    45. public static void main(String[] args) {
    46. MyThreadPool myThreadPool = new MyThreadPool(10);
    47. for (int i = 0; i < 100; i++) {
    48. myThreadPool.submit(new Runnable() {
    49. @Override
    50. public void run() {
    51. System.out.println("hello");
    52. }
    53. });
    54. }
    55. }
    56. }

    运行结果:

    呜呜呜,多线程的第四篇案例 —— 线程池 到这里就已经介绍完了~

    多线程的案例 到这里也已经撒花结束了~

    下一篇博客就会进入到 多线程进阶部分的内容啦~

     如果感觉这一篇博客对你有帮助的话,可以一键三连走一波,非常非常感谢啦 ~

  • 相关阅读:
    BetterDisplay Pro v2.0.11(显示器颜色校准软件)
    Flink部署——命令行界面
    C语言校园家教管理系统
    【web-攻击用户】(9.3.2)诱使用户执行操作--请求伪造——UI伪装
    力扣 -- 377. 组合总和 Ⅳ
    Web应用接入OAuth2
    Java连接池详解
    配置Swagger2生成API接口文档
    termux使用
    flask中的session伪造问题
  • 原文地址:https://blog.csdn.net/qq_53362595/article/details/126452153