• 阻塞队列和定时器的使用


    阻塞队列

    谈到队列,大家就能想到队列的先进先出原则,但有些特殊的队列,虽然也是先进先出的,但是带有阻塞功能,我们把这种队列叫做阻塞队列.

    ★如果队列为空,执行出队操作就会阻塞,阻塞到另外一个线程往队列里添加元素(队列不为空)为止.

    ★如果队列满了,执行入队操作时,也会阻塞,阻塞到另外一个线程从队列里取出元素(队列不满).

    想要了解阻塞队列,先了解生产者-消费者模型,这个模型给我们程序带来了非常重要的好处

    1.生产者-消费者模型

    在一个餐馆中,有多名厨师(生产者)负责烹饪菜品,并将其放在共享的出菜区(缓冲区)上。而服务员(消费者)则从出菜区上取出菜品并端给顾客。这里的厨师可以看作是生产者,出菜区是缓冲区,服务员是消费者。

    1. 实现了生产者和消费者之间的 "解耦"

    我们写代码要追求低耦合,所谓解耦操作,就是降低耦合的过程.

            比如过年一家人一起包饺子. 一般都是有明确分工 , 比如一个人负责擀饺子皮 , 其他人负责包 . 擀饺 子皮的人就是 "生产者", 包饺子的人就是 " 消费者 ".
            擀饺子皮的人不关心包饺子的人是谁( 能包就行 , 无论是手工包 , 借助工具 , 还是机器包 ), 包饺子的人 也不关心擀饺子皮的人是谁( 有饺子皮就行 , 无论是用擀面杖擀的 , 还是拿罐头瓶擀 , 还是直接从超市买的)
    上述操作生产者和消费者没有直接联系,而是通过阻塞队列相互关联,生产者和消费者有一方出现了问题,不会影响对方的操作,这样操作就降低了耦合.

    2.可以"削峰填谷",保证系统的稳定性

    阻塞队列就相当于一个缓冲区,平衡了生产者消费者的处理能力

    比如在 " 秒杀 " 场景下 , 服务器同一时刻可能会收到大量的支付请求 . 如果直接处理这些支付请求 , 服务器可能扛不住( 每个支付请求的处理都需要比较复杂的流程 ). 这个时候就可以把这些请求都放 到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求 .
    这样做可以有效进行 " 削峰 ", 防止服务器被突然到来的一波请求直接冲垮

    2.标准库中的阻塞队列

    java标准库中内置了阻塞队列,在使用的时候直接使用标准库中的就行

    1.BlockingDeque是一个接口

    2.真正实现BlockingDeque的类有三种

    3.带有阻塞功能的方法.put()方法用于阻塞队列的入队,take()方法用于阻塞队列的出队.在使用take的时候需要处理异常

    1. public static void main(String[] args) {
    2. BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
    3. //创建两个线程,来作为生产者和消费者
    4. Thread customer = new Thread(() -> {
    5. while (true) {
    6. try {
    7. Integer result = blockingQueue.take();
    8. System.out.println("消费元素: " + result);
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. }
    13. });
    14. customer.start();
    15. Thread producer = new Thread( () -> {
    16. int count = 0;
    17. while (true) {
    18. try {
    19. blockingQueue.put(count);
    20. System.out.println("生产元素: " + count );
    21. count++;
    22. Thread.sleep(500);
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. }
    26. }
    27. });
    28. producer.start();
    29. }

    上面执行的结果是

     上面代码先执行消费元素是因为当customer线程执行到blockingQueue,take()时发生了阻塞,等到producer线程执行blockingQueue.put()后,两个线程并发执行,谁先打印都有可能.

    4. 不带阻塞功能的方法 入队列 offer(), 出队列 poll(),  取队首元素 peek().

    3.模拟实现阻塞队列

    实现阻塞队列的前提是我们要先实现一个普通的队列,实现普通队列的方法有两种,一个是基于链表的队列,采用头删法和尾插法实现一个阻塞队列,另外一个方法是基于数组的循环队列,两个方法的具体实现在下面这篇文章里面.

    循环队列的实现.​​​​​​​

    要模拟实现阻塞队列,前提是在多线程的基础上,通过synchronized关键字对put()和take()方法中的代码进行加锁,然后通过wait()和notify()方法对插入元素和取出元素进行阻塞等待.具体方法是,当插入元素时如果队列满了,就产生阻塞等待,直到有元素取出后唤醒put中的wait().当取出元素的时候,如果队列为空,让take()方法产生阻塞等待,只到有元素插入后,唤醒take()中的wait().

    1. class MyBlockingQueue {
    2. private int[] item = new int[1000];
    3. private int head = 0;
    4. private int tail = 0;
    5. private int size = 0;
    6. //插入元素
    7. public void put(int value) throws InterruptedException {
    8. synchronized (this) {
    9. while(size == item.length) {
    10. //队列满了,产生阻塞等待
    11. this.wait();
    12. }
    13. item[tail] = value;
    14. tail++;
    15. if(tail >= item.length) {
    16. tail = 0;
    17. }
    18. size++;
    19. //唤醒take中的wait()
    20. this.notify();
    21. }
    22. }
    23. //取出元素
    24. public Integer take() throws InterruptedException {
    25. int result = 0;
    26. synchronized (this) {
    27. while(size == 0) {
    28. //队列为空,产生阻塞等待
    29. this.wait();
    30. }
    31. result = item[head];
    32. head++;
    33. if(head >= item.length) {
    34. head = 0;
    35. }
    36. size--;
    37. //唤醒put中的wait()
    38. this.notify();
    39. }
    40. return result;
    41. }
    42. }
    上面代码中put()和take()不会同时产生阻塞等待,因为我们是在同一个对象中,一个队列不可能同时满足队列为空和队列为满.

    定时器

    定时器就好比是我们生活中的闹钟,当我们被早八的闹钟叫醒后想在床上在躺十分钟,我们设置闹钟就可以让十分钟再响,而我们的计时器具有类似的功能,是指定一段之间后执行一段代码.

    在Java的标准库中有一个Timer类,Timer类的核心方法是schedule()

    schedule()中有连个参数,第一个参数指定定时器要执行的任务,第二个参数指定多长时间后执行这个任务

    观察代码结果发现,在 "程序启动" 打印后3000ms才打印  "运行定时器任务".

    模拟实现定时器

    主要实现下面几种功能

    1.让被注册的任务,能在指定时间后执行.为了实现这个功能可以单独在定时器内部建一个线程,让这个线程周期性的扫描,判断任务时间是否到了,如果到了,就执行,若没动,就继续等待.这n个任务,可以用优先级队列来保存,按照指定时间小的优先级高,就放在队头,此时扫描线程,就只需要扫描队头元素,对头元素时间没到的话,其他时间更不可能到.

    在优先级队列中,我们是基于时间进行比较的,可以实现 Comparable接口来进行比较

    在扫描线程中会出现下面的bug.

    2.一个定时器可以指定N个任务,N个任务会按照指定时间按顺序执行.

    具体实现代码

    1. class MyTask implements Comparable<MyTask>{
    2. //要执行的任务
    3. private Runnable runnable;
    4. //任务在什么时间执行(用毫秒时间戳表示)
    5. private long time;
    6. public MyTask(Runnable runnable, long time) {
    7. this.runnable = runnable;
    8. this.time = time;
    9. }
    10. //获取当前任务的时间
    11. public long getTime() {
    12. return time;
    13. }
    14. //执行任务
    15. public void run() {
    16. runnable.run();
    17. }
    18. @Override
    19. public int compareTo(MyTask o) {
    20. return (int) (this.time - o.time);
    21. }
    22. }
    23. //定时器
    24. class MyTimer {
    25. //扫描线程
    26. private Thread t = null;
    27. //有一个阻塞优先级队列
    28. private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    29. public MyTimer() {
    30. t = new Thread(() -> {
    31. while (true) {
    32. try {
    33. synchronized (this) {
    34. MyTask myTask = queue.take();
    35. //取出队头元素观察判断是否到时间了
    36. long curTime = System.currentTimeMillis();
    37. if(curTime < myTask.getTime()) {
    38. //时间没到放回队列
    39. queue.put(myTask);
    40. } else {
    41. //时间到了,执行任务
    42. myTask.run();
    43. this.wait(myTask.getTime() - curTime);
    44. }
    45. }
    46. } catch (InterruptedException e) {
    47. e.printStackTrace();
    48. }
    49. }
    50. } );
    51. t.start();
    52. }
    53. public void schedule (Runnable runnable,long after) {
    54. //第一个参数 是指定的任务
    55. // 第二个参数是在多少毫秒后执行
    56. MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+ after);
    57. //把任务放到队列里
    58. queue.put(myTask);
    59. synchronized (this) {
    60. this.notify();
    61. }
    62. }

  • 相关阅读:
    【Spring】Spring的JdbcTemplate
    Python对象的内置方法(__del__和__str__)的介绍和使用
    字符串哈希代码模板
    执行Maven项目时,无法解析项目的依赖关系
    建模助手(Revit)插件如何生成基坑开挖和地下连续墙?
    Java基础入门day62
    【ArcGIS】基本概念-矢量空间分析
    Xline 源码解读(三) —— CURP Server 的实现
    【Spring框架】Spring概述及基本应用
    css:button实现el-radio效果
  • 原文地址:https://blog.csdn.net/2301_76692760/article/details/134273836