• 06.JAVAEE之线程4


    1.定时器

    1.1 定时器是什么

    定时器也是软件开发中的一个重要组件.
    类似于一个 " 闹钟 ". 达到一个设定的时间之后 , 就执行某个指定好的代码.
    约定一个时间,时间到达之后,执行某个代码逻辑,
    定时器非常常见,尤其是在进行网络通信的时候,

     需要有等待的最大时间,等待的最大时间通过定时器实现。

    在标准库里,也是有现成的定时器的实现的 

    主线程执行 schedule 方法的时候,就是把这个任务给放到 timer 对象中了,
    于此同时,timer 里头也包含一个线程,这个线程叫做"扫描线程”,一旦时间到,扫描线程就会执行刚才安排的任务了。
    仔细观察,可以发现,整个进程其实没有结束!! 就是因为 Timer 内部的线程,阻止了进程结束.
    Timer 里,是可以安排多个任务的
    1. import java.util.Timer;
    2. import java.util.TimerTask;
    3. // 定时器
    4. public class Demo25 {
    5. public static void main(String[] args) {
    6. Timer timer = new Timer();
    7. // 给定时器安排了一个任务, 预定在 xxx 时间去执行.
    8. timer.schedule(new TimerTask() {
    9. @Override
    10. public void run() {
    11. System.out.println("3000");
    12. }
    13. }, 3000);
    14. timer.schedule(new TimerTask() {
    15. @Override
    16. public void run() {
    17. System.out.println("2000");
    18. }
    19. }, 2000);
    20. timer.schedule(new TimerTask() {
    21. @Override
    22. public void run() {
    23. System.out.println("1000");
    24. }
    25. }, 1000);
    26. System.out.println("程序启动!");
    27. }
    28. }

     1.2 如何实现定时器

    Timer timer = new Timer();

    1.Timer 中需要有一个线程,扫描任务是否到时间,可以执行了

    2.需要有一个数据结构,把所有的任务都保存起来.

    【具体使用什么数据结构好呢?

    假设使用数组(ArrayList),此时,扫描线程, 就需要不停的遍历数组中的每个任务判定每个任务是否都到达执行时间.

    使用优先级队列,是更好的办法!!给 Timer 中添加的这些任务, 都是带有一个"时间’定是时间小的先执行。最先执行的就是时间最小的任务!!如果时间最小的任务,还没到时间呢,其他任务更不会到时间了!!优先级队列,可以使用 O(1)时间,来获取到时间最小的任务的!!

    3.还需要创建,个类,通过类的对象来描述一个任务.(至少要包含任务内容和时间)

    使用绝对的时间戳更为方便

    对于优先级队列,要求里面的元素是可比较的,所以需要重写比较方法。

    • 如果发现队列为空,应该咋办呢?

    好的办法,就是阻塞等待,等到队列不空为止 =>阻塞队列不就是这样的嘛~~
    wait 要想使用, 需要搭配 synchronized,不能单独使用!!wait 进行的操作有三个:
    1)释放锁 =>前提是, 先拿到锁,
    2)等待通知
    3) 通知到来之后, 唤醒,重新获取锁
    这个方法,是一个线程中(比如主线程中),给队列添加元素

    出现忙等时

    忙等的过程,确实在等,但是也消耗了很多 cpu

    1个notify起到两个xiaog

    之所以咱们的代码,使用的是 PriorityQueue,而不是 PriorityBlockingQueue,其实就是因为要处理两个 wait 的地方使用阻塞版本的优先级队列,不方便实现这样的两处等待~~

    1. mport java.util.PriorityQueue;
    2. // 通过这个类, 描述了一个任务
    3. class MyTimerTask implements Comparable {
    4. // 要有一个要执行的任务
    5. private Runnable runnable;
    6. // 还要有一个执行任务的时间
    7. private long time;
    8. // 此处的 delay 就是 schedule 方法传入的 "相对时间"
    9. public MyTimerTask(Runnable runnable, long delay) {
    10. this.runnable = runnable;
    11. this.time = System.currentTimeMillis() + delay;
    12. }
    13. @Override
    14. public int compareTo(MyTimerTask o) {
    15. // 这样的写法, 就是让队首元素是最小时间的值
    16. // 到底是谁 - 谁, 不要背!! 你可以试试!!
    17. return (int) (this.time - o.time);
    18. // 如果是想让队首元素是最大时间的值
    19. // return o.time - this.time;
    20. }
    21. public long getTime() {
    22. return time;
    23. }
    24. public Runnable getRunnable() {
    25. return runnable;
    26. }
    27. }
    28. // 咱们自己搞的定时器
    29. class MyTimer {
    30. // 使用一个数据结构, 保存所有要安排的任务.
    31. private PriorityQueue queue = new PriorityQueue<>();
    32. // 使用这个对象作为锁对象.
    33. private Object locker = new Object();
    34. public void schedule(Runnable runnable, long delay) {
    35. synchronized (locker) {
    36. queue.offer(new MyTimerTask(runnable, delay));
    37. locker.notify();
    38. }
    39. }
    40. // 搞个扫描线程.
    41. public MyTimer() {
    42. // 创建一个扫描线程
    43. Thread t = new Thread(() -> {
    44. // 扫描线程, 需要不停的扫描队首元素, 看是否是到达时间.
    45. while (true) {
    46. try {
    47. synchronized (locker) {
    48. // 不要使用 if 作为 wait 的判定条件, 应该使用 while
    49. // 使用 while 的目的是为了在 wait 被唤醒的时候, 再次确认一下条件.
    50. while (queue.isEmpty()) {
    51. // 使用 wait 进行等待.
    52. // 这里的 wait, 需要由另外的线程唤醒.
    53. // 添加了新的任务, 就应该唤醒.
    54. locker.wait();
    55. }
    56. MyTimerTask task = queue.peek();
    57. // 比较一下看当前的队首元素是否可以执行了.
    58. long curTime = System.currentTimeMillis();
    59. if (curTime >= task.getTime()) {
    60. // 当前时间已经达到了任务时间, 就可以执行任务了
    61. task.getRunnable().run();
    62. // 任务执行完了, 就可以从队列中删除了.
    63. queue.poll();
    64. } else {
    65. // 当前时间还没到任务时间, 暂时不执行任务.
    66. // 暂时先啥都不干, 等待下一轮的循环判定了.
    67. locker.wait(task.getTime() - curTime);
    68. }
    69. }
    70. } catch (InterruptedException e) {
    71. e.printStackTrace();
    72. }
    73. }
    74. });
    75. t.start();
    76. }
    77. }
    78. public class Demo26 {
    79. public static void main(String[] args) {
    80. MyTimer timer = new MyTimer();
    81. timer.schedule(new Runnable() {
    82. @Override
    83. public void run() {
    84. System.out.println("3000");
    85. }
    86. }, 3000);
    87. timer.schedule(new Runnable() {
    88. @Override
    89. public void run() {
    90. System.out.println("2000");
    91. }
    92. }, 2000);
    93. timer.schedule(new Runnable() {
    94. @Override
    95. public void run() {
    96. System.out.println("1000");
    97. }
    98. }, 1000);
    99. System.out.println("程序开始执行");
    100. }
    101. }

     2.线程池

    线程诞生的意义,是因为进程的创建/销毁, 太重量了(比较慢)有对比,才有伤害,和进程比,线程,是更快了,但是如果进一步提高创建销毁的频率, 线程的开销也不能忽视了!

    两种典型的办法,进一步提高效率:

    1.协程(轻量级线程)

    相比于线程,把系统调度的过程,给省略了.(程序猿手工调度当下,一种比较流行的并发编程的手段. 但是在 Java 圈子里,协程还不够流行.

    2.线程池

    线程池最大的好处就是减少每次启动、销毁线程的损耗。
    在使用第一个线程的时候,提前把 2345..线程创建好 (培养感情)
    后续如果想使用新的线程,不必重新创建了,直接拿过来就能用!!!(此时创建线程的开销就被降低了)

    2.1 线程池的使用 

    把线程创建好,放在池子里,后续用的时候直接从池子里来取~~

    为什么从池子取, 的效率比新创建线程,效率更高??? 

    从池子取,这个动作,是纯粹用户态的操作.

    创建新的线程,这个动作,则是需要 用户态 +内核态 相互配合,完成的操作

     出现的问题

    很多时候 构造一个对象,希望有多种构造方式.
    多种方式,就需要使用多个版本的构造方法来分别实现.
    但是构造方法要求方法的名字必须是类名,不同的构造方法,就只能通过 重载 的方式来区分了.(重载 =>参数类型/个数 不同)

    上面两个代码并没有构成重载,故编译失败。

    工厂设计模式

    解决方案:使用工厂设计模式

    使用工厂设计模式,就能解决这个问题.
    使用普通的方法,代替构造方法完成初始化工作.普通方法就可以使用方法的名字来区分了.也就不再收到重载的规则制约了
    实践中, 一般单独搞一个类,给这个类搞一些静态方法, 由这样的静态方法负责构造出对象。

    使用不同的方法名做出区分

    1. import java.util.concurrent.ExecutorService;
    2. import java.util.concurrent.Executors;
    3. public class Demo27 {
    4. public static void main(String[] args) {
    5. ExecutorService service = Executors.newFixedThreadPool(4);
    6. service.submit(new Runnable() {
    7. @Override
    8. public void run() {
    9. System.out.println("hello");
    10. }
    11. });
    12. }
    13. }

    Executors 创建线程池的几种方式

    • newFixedThreadPool: 创建固定线程数的线程池
    • newCachedThreadPool: 创建线程数目动态增长的线程池.
    • newSingleThreadExecutor: 创建只包含单个线程的线程池. 
    • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer. 

    上述这几个工厂方法生成的线程池,本质上都是对一个 类 进行的封装,ThreadPoolExecutor这个类,功能非常丰富,提供了很多参数,标准库上述的几个工厂方法,其实就是给这个类填写了不同的参数用来构造线程池了【Executors 本质上是 ThreadPoolExecutor 类的封装】

    • 不同的拒绝策略有不同的效果

    使用线程池,需要设置线程的数目.数目设置多少合适??

    ExecutorService service = Executors.newFixedThreadPool(4);

    在接触到实际代码之前是无法确定的。

    一个线程,执行的代码,主要有两类:
    1.cpu 密集型: 代码里主要的逻辑是在进行 算术运算/逻辑判断

    2.IO密集型: 代码里主要进行的是 Io操作,
    假设一个线程的所有代码都是 cpu 密集型代码,这个时候,线程池的数量不应该超过 N(设置 N 就是极限了)设置的比 N 更大,这个时候,也无法提高效率了.(cpu 吃满了)此时更多的线程反而增加调度的开销.
    假设一个线程的所有代码都是 Io密集的,这个时候不吃 CPU,此时设置的线程数,就可以是超过 N.较大的值一个核心可以通过调度的方式,来并发执行~

    我们就可以知道:

    代码不同, 线程池的线程数目设置就不同,无法知道一个代码,具体多少内容是 cpu 密集, 多少内容是Io密集

    正确做法: 使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程数目,看哪种情况下,最符合要求

     2.2 线程池的实现

    1. import java.util.concurrent.ArrayBlockingQueue;
    2. import java.util.concurrent.BlockingQueue;
    3. class MyThreadPool {
    4. // 任务队列
    5. private BlockingQueue queue = new ArrayBlockingQueue<>(1000);
    6. // 通过这个方法, 把任务添加到队列中
    7. public void submit(Runnable runnable) throws InterruptedException {
    8. // 此处咱们的拒绝策略, 相当于是第五种策略了. 阻塞等待~~ (这是下策)
    9. queue.put(runnable);
    10. }
    11. public MyThreadPool(int n) {
    12. // 创建出 n 个线程, 负责执行上述队列中的任务.
    13. for (int i = 0; i < n; i++) {
    14. Thread t = new Thread(() -> {
    15. // 让这个线程, 从队列中消费任务, 并进行执行.
    16. try {
    17. Runnable runnable = queue.take();
    18. runnable.run();
    19. } catch (InterruptedException e) {
    20. e.printStackTrace();
    21. }
    22. });
    23. t.start();
    24. }
    25. }
    26. }
    27. public class Demo28 {
    28. public static void main(String[] args) throws InterruptedException {
    29. MyThreadPool myThreadPool = new MyThreadPool(4);
    30. for (int i = 0; i < 1000; i++) {
    31. int id = i;
    32. myThreadPool.submit(new Runnable() {
    33. @Override
    34. public void run() {
    35. System.out.println("执行任务: " + id);
    36. }
    37. });
    38. }
    39. }
    40. }

    主线:

    线程概念 -> Thread 用法 ->线程安全问题 ->wait notify -> 线程案例

  • 相关阅读:
    超越所有MIM模型的BEiT v2来了!微软使用矢量量化视觉Tokenizers的掩码图像建模!
    java-net-php-python-s2sh教学管理平台hsg8229AGA2录像计算机毕业设计程序
    C++(拷贝构造与赋值重载)
    Java、泛型归并排序
    基于OpenCV的灰度图的图片相似度计算
    Laravel安装Passport,安装laravel-permission报错max key length
    leetcode - 438. Find All Anagrams in a String
    运维那些事儿|2023年,运维还有出路吗?
    [软件工具]ARW文件批量转图片jpg工具使用教程
    2023如何做谷歌收录?
  • 原文地址:https://blog.csdn.net/m0_47017197/article/details/138170292