Java多线程3--设计模式,线程池,定时器

Java多线程3--设计模式,线程池,定时器

  • 一级目录
  • Java多线程3
    • [1. 设计模式--单例模式](#1. 设计模式--单例模式)
      • [1.1 啥是设计模式?](#1.1 啥是设计模式?)
      • [1.2 单例模式的分类](#1.2 单例模式的分类)
        • [1.2.1 饿汉模式](#1.2.1 饿汉模式)
        • [1.2.2 懒汉模式-单线程版](#1.2.2 懒汉模式-单线程版)
      • [1.3 单例模式线程安全性的问题](#1.3 单例模式线程安全性的问题)
      • [1.4 加锁引入的新问题](#1.4 加锁引入的新问题)
    • [2. 阻塞队列](#2. 阻塞队列)
      • [2.1 ==阻塞队列是什么==](#2.1 ==阻塞队列是什么==)
      • [2.2 生产者消费者模型](#2.2 生产者消费者模型)
        • [2.2.1 优势一](#2.2.1 优势一)
        • [2.2.2 优势二](#2.2.2 优势二)
        • [2.2.3 生产者消费者模型的缺点](#2.2.3 生产者消费者模型的缺点)
      • [2.3 标准库中的阻塞队列](#2.3 标准库中的阻塞队列)
      • [2.4 生产者消费者模型的实现](#2.4 生产者消费者模型的实现)
      • [2.5 阻塞队列的实现](#2.5 阻塞队列的实现)
    • [3. 线程池](#3. 线程池)
      • [3.1 线程池是什么](#3.1 线程池是什么)
      • [3.2 标准库中的线程池](#3.2 标准库中的线程池)
      • [3.3 线程池的模拟实现](#3.3 线程池的模拟实现)
    • [4. 定时器](#4. 定时器)
      • [4.1 定时器是什么](#4.1 定时器是什么)
      • [4.2 标准库中的定时器](#4.2 标准库中的定时器)
      • [4.3 实现定时器](#4.3 实现定时器)

一级目录

二级目录

三级目录

Java多线程3

1. 设计模式--单例模式

1.1 啥是设计模式?

设计模式好比象棋中的棋谱. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.

软件开发中也有很多常见的 "问题场景". 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.

1.2 单例模式的分类

具体的实现方式有很多. 最常见的是 "饿汉" 和 "懒汉" 两种

1.2.1 饿汉模式

类加载的同时, 创建实例

java 复制代码
class Singleton {
 private static Singleton instance = new Singleton();
 private Singleton() {}
 public static Singleton getInstance() {
 return instance;
 }
}

对于代码中细节的讲解

1.2.2 懒汉模式-单线程版

类加载的时候不创建实例. 第一次使用的时候才创建实例.

java 复制代码
class Singleton {
比特就业课
 private static Singleton instance = null;
 private Singleton() {}
 public static Singleton getInstance() {
 if (instance == null) {
 instance = new Singleton();
 }
 return instance;
 }
}

懒和饿是相对的

饿是今早创建实例(类加载的过程中)

懒是尽晚的创建实例(甚至可能不创建了,延迟创建)

1.3 单例模式线程安全性的问题

  1. 上面谈到的单例模式,和线程有什么关系?
  2. 刚才编写的两份代码(饿汉,懒汉),是否是线程安全的?如果不是,该怎么办?


上面的懒汉模式的实现是线程不安全的

线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.

一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改instance 了)

加上 synchronized 可以改善这里的线程安全问题

java 复制代码
class Singleton {
	 private static Singleton instance = null;
	 private Singleton() {}
	 
	 public static Singleton getInstance() {
	 	synchronized(locker){
		 	if (instance == null) {
			 instance = new Singleton();
			 }
	 	} 
		return instance;
	 }
}

这样的作用

以下的写法和上面的写法相同

java 复制代码
class Singleton {
	 private static Singleton instance = null;
	 private Singleton() {}
	 public synchronized static Singleton getInstance() {
		 if (instance == null) {
		 	instance = new Singleton();
		 }
	 return instance;
	 }
}

这个写法,相当于把locker对象换成了类对象SingletonLazy.class和之前的locker相比,没什么区别

1.4 加锁引入的新问题

java 复制代码
public synchronized static Singleton getInstance() {
		 if (instance == null) {
		 	instance = new Singleton();
		 }
	 return instance;
}

当我们把实例创建好了之后,后续再调用getInstance,此时都是直接执行return,如果只是进行ifpanding+return,纯粹的读操作了。读操作,不涉及到线程安全问题。

但是,每次调用上述的方法,都会触发一次加锁操作,虽然不涉及线程安全问题了,多线程的情况下,这里的加锁,就会互相阻塞-->影响程序的执行效率

因此,我们的解决方案是按需加锁,涉及到线程安全的时候再加锁,不涉及线程安全的时候,就不加锁

java 复制代码
public static SingletonLazy getInstance(){
        if(instance==null){
            synchronized (locker){
                if(instance==null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
}

以下是对于这段代码的解析:

以往都是单线程的程序,单线程中,连续的两个相同的if,是无意义的,单线程中,执行流就只有一个,上一个if的判定结果和下一个if是一样的

但是在多线程程序中,两次判定之间,可能存在其他线程,就把if中的Instance变量给修改了,也就导致这里的两次if结论可能不同

因此,再仔细分析,上述代码依然存在问题

这里存在指令重排的问题,也是编译器优化的一种体现形式,编译会在逻辑不变的前提下,调整你代码执行的先后顺序,以达到提升程序运行效率的效果。

java 复制代码
public static SingletonLazy getInstance(){
        if(instance==null){
            synchronized (locker){
                if(instance==null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
}

这里的instance = new SingletonLazy();有三个原子步骤

  1. 申请内存空间
  2. 再空间上面构造对象(初始化)
  3. 内存空间的首地址,赋值给引用变量

正常来说,这三个步骤按照123这样的顺序来执行

但是,在指令重排序下,可能称为123这样的顺序,单线程环境下,123还是132其实没有问题,但是在多线程环境下可能出现bug!!!

我们假设有两个线程,线程 A 和线程 B,同时调用 getInstance(): 线程 A 进入 getInstance(),发现

instance 为 null,进入 synchronized 块。 线程 A 执行 new

SingletonLazy(),但发生了指令重排

  1. 它先分配了内存。
  2. 然后把这个未初始化的内存地址赋值给了 instance。
  3. 就在它正要执行第三步 "初始化对象" 的时候,线程 A 被 CPU 调度器挂起了(A仍然持有锁locker)

线程 B 此时也调用 getInstance(),执行到第一次检查 if (instance == null)。
它看到 instance 已经不是 null 了(因为线程 A 已经把地址赋值过去了),于是直接跳过 synchronized 块,返回了这个 instance。 线程 B 拿到这个 instance 后,立刻调用 s.func()。

但此时,这个对象的构造函数还没执行,所有成员变量都还是默认值(比如 0、null)。

解决方案:在懒汉类中加入

java 复制代码
private  static volatile SingletonLazy instance = null;

因此,总的正确代码为:

java 复制代码
class SingletonLazy{
    private  static volatile SingletonLazy instance = null;

    private static Object locker = new Object();

    //在懒汉模式下,创建实例的时机,是在第一次使用的时候。而不是在程序启动的时候
    public static SingletonLazy getInstance(){
        if(instance==null){
            synchronized (locker){
                if(instance==null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy(){

    }
}

我们总结一下volatile的功能:

  1. 保证每次读取操作都是都内存,这个问题在Java多线程2--Java的线程安全问题当中有详细的解析
  2. 关于该变量的读取和修改操作,不会触发重排序

2. 阻塞队列

2.1 阻塞队列是什么

阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则.

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

• 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.

• 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型.

2.2 生产者消费者模型

生产者消费者模式就是通过一个容器 来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

生产者消费者模型的两个重要优势

2.2.1 优势一

解耦合(不一定是两个线程之间,也可能是两个服务器之间)

2.2.2 优势二

削峰填谷

一般来说A这种上游服务器,尤其是入口的服务器,干的活更简单,单个请求消耗资源数少,像B这种下游的服务器,通常承担更重的任务量,复杂的计算/存储任务,单个请求消耗资源数量更多。

日常工作中,确实是会给B这样角色的服务器分配更好的机器,即使如此,也很难保证B承担的访问量能够比A更高。

因此,削峰填谷 策略就十分的奏效

2.2.3 生产者消费者模型的缺点

生产者消费者模型肯定也会相应的付出一些代价

  1. 引入队列之后,整体的结构会更复杂,此时,就需要更对的机器,进行部署,生产环境的结构会更复杂,管理起来更麻烦
  2. 效率会有影响

2.3 标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.

• BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.

• put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.

• BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

java 复制代码
public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        queue.put("abc");
        String elem = queue.take();
        System.out.println(elem);
}

运行结果为:

java 复制代码
public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> queue = new LinkedBlockingDeque<>(100);
        //带有阻塞
        for (int i = 0; i < 100; i++) {
            queue.put("aaa");
        }
        System.out.println("队列已经满了");
        queue.put("aaa");

//        String elem = queue.take();

        System.out.println("再次尝试put元素");
    }


即在第16行被阻塞

注意:

java 复制代码
new LinkedBlockingDeque<>(100);

如果不设置capacity,默认是一个非常大的数值。。。

实际开发中,一般建议大家能够设置上你要求的最大值,否则你的队列可能变得非常大,导致把内存耗尽,产生内存超出范围这样的异常

2.4 生产者消费者模型的实现

java 复制代码
public static void main(String[] args) {
        BlockingDeque<Integer> queue = new LinkedBlockingDeque<>(1000);

        //至少生产者一个线程,消费者一个线程
        Thread producer = new Thread(()->{
            int n = 0;
            while(true){
                try {
                    queue.put(n);
                    System.out.println("生产元素 "+n);
                    n++;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"producer");
        Thread consumer = new Thread(()->{
            while(true){
                try {
                    Integer n = queue.take();
                    System.out.println("消费元素 "+n);
                    Thread.sleep(1000);

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"consumer");

        producer.start();
        consumer.start();
    }

在该程序中,生产者会迅速填满阻塞队列的1000个空间,消费者由于sleep操作,每个约一秒取元素一次,此时生产与消费元素达到了平衡

运行结果为:

2.5 阻塞队列的实现

java 复制代码
class MyBlockingQueue{
    public int head = 0;

    public int tail = 0;

    public int size = 0;

    public String[] data = null;

    public MyBlockingQueue(int capacity){
        data = new String[capacity];
    }

    public void put(String elem) throws InterruptedException {
        synchronized (this){
            while(size>=data.length){
                //队列满了,需要阻塞
                this.wait();
            }
            data[tail] = elem;
            tail = (tail+1)%data.length;
            size++;
            this.notify();
        }
    }

    public String take() throws InterruptedException {
        synchronized (this){
            while(size==0){
                this.wait();
            }
            String elem = data[head];
            head++;
            if(head>=data.length){
                head = 0;
            }
            size--;
            this.notify();
            return elem;
        }
    }
}

public class Demo31 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue(1000);

        Thread producer = new Thread(()->{
            int n = 0;
            while(true){
                try {
                    queue.put(n+"");
                    System.out.println("生产元素 "+n);
                    n++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread consumer = new Thread(()->{
           while(true){
               String n = null;
               try {
                   n = queue.take();
                   System.out.println("消费元素 "+n);

               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        producer.start();
        consumer.start();
    }
}

运行结果:

下图是对于代码中wait/notify操作的解析图

这里还有一个重要的问题,需要注意

API 说明:

在等待时,推荐的做法是在调用 wait 的代码外围,用一个 while 循环来检查等待的条件是否满足,如下例所示。这种方式可以避免由 虚假唤醒 等问题带来的风险。

java 复制代码
synchronized (obj) {
    while (<条件不成立> && <超时未到>) {
        long timeoutMillis = ...; // 重新计算超时值
        int nanos = ...;
        obj.wait(timeoutMillis, nanos);
    }
    ... // 根据条件或超时情况执行相应操作
}

spurious wakeups(虚假唤醒):在没有调用 notify()/notifyAll() 的情况下,线程从 wait() 中被唤醒的现象。这是操作系统层面的正常现象,因此必须用 while 循环重新检查条件,确保唤醒是 "真的" 满足了等待条件。

condition being awaited(等待的条件):线程等待的目标条件(例如 "队列非空""任务到执行时间" 等)。

应为当我们的wait被唤醒后(表明此时size>0),但是仍然存在其他线程执行take操作,使得size0 的情况,因此,需要再确认一次size0, 因此这里使用while循环,而不是if条件语句

3. 线程池

3.1 线程池是什么

我们可以把线程池理解成一个 "线程工厂 + 线程管理中心"

没有线程池时:每次需要执行任务,都要 新建一个线程→执行任务→销毁线程 ,就像每次打车都要 "造一辆新车→开车→报废车",成本极高。

有了线程池后:线程池会提前 创建一批线程并保存起来核心线程,接到任务时直接分配空闲线程执行,任务完成后线程不销毁,放回池子里等待下一个任务;如果任务太多 ,还会临时创建线程非核心线程,任务少了再销毁这些临时线程。

简单来说:线程池是管理线程的 池子,核心目的是复用线程、控制线程数量、降低资源消耗。

3.2 标准库中的线程池

• 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.

• 返回值类型为 ExecutorService

• 通过 ExecutorService.submit 可以注册一个任务到线程池中.

java 复制代码
public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            int id = i;
            pool.submit(()->{
                System.out.println("hello "+id+","+Thread.currentThread().getName());
            });
        }
    }

我们创建了10个线程,分别分派这10个线程去执行100个提交到线程池中的任务(Runnable类型)

java 复制代码
pool.submit(()->{
                System.out.println("hello "+id+","+Thread.currentThread().getName());
});

就等同于如下代码,也就是提交一个新创建出来的Runnable类型的任务

java 复制代码
pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello "+id+","+Thread.currentThread().getName());
                }
});

运行结果为:

。。。。

Executors 创建线程池的几种方式

• newFixedThreadPool: 创建固定线程数的线程池

• newCachedThreadPool: 创建线程数目动态增长的线程池,最大线程数可以是一个很大的数字.

• newSingleThreadExecutor: 创建只包含单个线程的线程池.

• newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer. Executors 本质上是 ThreadPoolExecutor 类的封装

Java标准库中提供了可以直接使用的线程池

经典面试考题:方法中的7个参数都是什么意思

  1. int corePoolSize 核心线程数,即使这些线程处于空闲状态(没任务执行),也不会被销毁

  2. int maximunPoolSize 线程池允许创建的总线程数上限 即 核心线程+非核心线程数,只有当核心线程全忙 + 任务队列满时,线程池才会创建非核心线程。

  3. long keepAliveTime 非核心线程处于空闲状态(没有执行任务)的最大时长,超过这个时间,非核心线程会被销毁

  4. TimeUnit unit: 枚举

  5. BlockingQueue workQueue: 工作队列,线程池本质上也是生产者消费者模型。调用submit就是在生产任务,线程池里的线程就是在消费任务

  6. ThreadFactory threadFactory: 工厂模式,给线程类提供的工厂类

  7. RejectedExecutionHandler handler: 拒绝策略。submit把任务添加到任务队列中,任务队列是阻塞队列,当队列满了,此时再添加任务,对于线程池来说 ,不会触发真正的任务入队列操作,不会真阻塞,而是执行拒绝策略相关的代码。


    1.ThreadPoolExecutor.AbortPolicy(默认策略)
    行为 :直接抛出 RejectedExecutionException 异常,阻止任务提交。
    特点 : 最严格的策略,一旦触发就会中断提交流程;如果不捕获异常,会导致提交任务的线程直接崩溃,线程池也可能无法继续工作。
    适用场景:对任务执行可靠性要求极高,不允许任务丢失,且能接受抛出异常的场景。

    2.ThreadPoolExecutor.CallerRunsPolicy 行为:让提交任务的线程(比如主线程)自己去执行这个任务,而不是交给线程池。 特点:

    不会丢弃任务,也不会抛异常;会降低提交任务的线程的性能(因为它要去执行任务),相当于 "自我限流";如果线程池已经关闭,任务会被丢弃。

    适用场景:对性能要求不敏感,但不允许任务丢失的场景,比如日志记录、简单统计等。

    3. ThreadPoolExecutor.DiscardOldestPolicy 行为:丢弃任务队列中最老的、还没被执行的任务 ,然后重新尝试提交当前这个新任务。 特点: 牺牲 "旧任务" 来给 "新任务" 腾位置;

    如果线程池已经关闭,新任务会被直接丢弃。 适用场景:任务时效性强,旧任务已经没有执行价值,优先保证新任务执行的场景(比如实时数据更新)。

    4. ThreadPoolExecutor.DiscardPolicy 行为:直接静默丢弃当前提交的这个新任务 ,不抛异常,也不做任何处理。 特点: 最 "佛系"

    的策略,完全不处理,提交者甚至不知道任务被丢了; 风险极高,容易导致业务数据丢失且难以排查。

    适用场景:任务执行与否对业务影响极小,比如心跳检测、统计采样等可丢失的任务。

    总结一下:

注:

工厂模式(简单工厂)的核心:把对象创建逻辑抽离到工厂类,调用者通过工厂方法获取对象,无需关注创建细节;

工厂模式解决的问题

我们使用构造方法的名字是固定的,要想提供不同的版本,就要通过重载,而有时候不一定能构成重载,而使用工厂模式就可以就可以解决提供参数类型相同(无法重载的)的不同版本的方法的问题

java 复制代码
// 点类(产品类):封装点的属性,隐藏构造细节(可选,增强封装性)
class Point {
    // 私有化属性,通过getter访问,符合封装原则
    private final double x;
    private final double y;

    // 私有化构造方法:强制外部通过工厂类创建对象(工厂模式的进阶优化)
    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    // 提供getter方法访问属性
    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    // 重写toString,方便打印输出
    @Override
    public String toString() {
        return String.format("(x=%.2f, y=%.2f)", x, y);
    }
}

// 工厂类:封装所有Point对象的创建逻辑
class PointFactory {
    /**
     * 通过直角坐标(x,y)创建点
     * @param x 横坐标
     * @param y 纵坐标
     * @return 直角坐标对应的Point对象
     */
    public static Point makePointByXY(double x, double y) {
        // 直接创建直角坐标点,逻辑简单
        return new Point(x, y);
    }

    /**
     * 通过极坐标(r,a)创建点
     * @param r 极径(到原点的距离)
     * @param a 极角(角度,如30°、90°,非弧度)
     * @return 极坐标转换后的Point对象
     */
    public static Point makePointByRA(double r, double a) {
        // 核心修复:极坐标转直角坐标的数学公式
        // 步骤1:将角度转换为弧度(Math的三角函数默认使用弧度)
        double radians = Math.toRadians(a);
        // 步骤2:极坐标转直角坐标公式
        double x = r * Math.cos(radians);
        double y = r * Math.sin(radians);
        // 步骤3:创建并返回Point对象
        return new Point(x, y);
    }
}

// 测试类
public class Demo33 {
    public static void main(String[] args) {
        // 1. 通过直角坐标创建点
        Point pointXY = PointFactory.makePointByXY(10, 20);
        System.out.println("直角坐标创建的点:" + pointXY);

        // 2. 通过极坐标创建点(r=10,角度30°)
        Point pointRA = PointFactory.makePointByRA(10, 30);
        System.out.println("极坐标(10,30°)转换的点:" + pointRA);

        // 3. 极坐标测试:r=5,角度90°(预期x=0, y=5)
        Point pointRA2 = PointFactory.makePointByRA(5, 90);
        System.out.println("极坐标(5,90°)转换的点:" + pointRA2);
    }
}

运行结果为:

3.3 线程池的模拟实现

java 复制代码
class MyThreadPool{
    //利用阻塞存储任务,最多可以存储1000个任务
    BlockingQueue<Runnable> queue = null;
    //创建n个线程
    public MyThreadPool(int n){
        queue = new LinkedBlockingQueue<>(1000);
        for (int i = 0; i < n; i++) {

            Thread t = new Thread(()->{
                //让每个线程持续获得阻塞队列中的任务,并执行
                while(true) {
                    try {
                        Runnable task = queue.take();
                        task.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();

        }
    }

    public void submit(Runnable task) throws InterruptedException {
        queue.put(task);
    }
}


public class Demo35 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            int id = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("id = "+id+Thread.currentThread().getName());
                }
            });
        }

    }
    
}

该代码执行的流程为:

  1. 首先创先线程池(MyThreadPool),并初始化,由于任务队列中现在没有任务,Runnable task = queue.take();代码运行到该语句处阻塞
  2. 创建100个Runnable类型的任务并提交
  3. 当阻塞队列中有任务时,就开始创建线程,并利用已经创建的10个线程循环执行任务

运行结果为:

线程池的作用:

4. 定时器

4.1 定时器是什么

定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.

4.2 标准库中的定时器

• 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .

• schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).

java 复制代码
public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        },1000);
        System.out.println("hello main");
    }

运行结果为:

4.3 实现定时器

定时器的构成

• 创建一个类,表示一个任务

• 定时器中,必须使用一些集合类把这多个任务给管理起来--优先级队列

• 实现schedule方法:把任务添加到队列中即可

• 额外创建一个线程,负责执行队列中的任务(和线程池不同,线程池是只要队列不为空,就立即取任务并执行,此处需要看队首元素的时间,是否到了。时间到,才能执行,时间不到,不能执行)。

java 复制代码
class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable task;
    private long time;

    MyTimerTask(Runnable task, long time){
        this.task = task;
        this.time = time;
    }

    @Override
    public int compareTo(MyTimerTask o){
        return (int)(this.time - o.time);
    }

    public long getTime(){
        return time;
    }

    public void run(){
        task.run();
    }

}

class MyTimer{
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    Object locker = new Object();
    public void schedule(Runnable task, long delay){
        synchronized (locker){
            MyTimerTask myTimerTask = new MyTimerTask(task, delay+System.currentTimeMillis());
            queue.offer(myTimerTask);
            locker.notify();
        }

    }

    public MyTimer(){
        Thread t = new Thread(()->{
            while(true){
                synchronized (locker){
                    try {
                        while(queue.isEmpty()){
                            //当队列不为空就唤醒
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        //当前的时间小于任务执行的时间,等待,有新任务来的时候会被唤醒
                        if(task.getTime()>System.currentTimeMillis()){
                            locker.wait(task.getTime()-System.currentTimeMillis());
                        }
                        else{
                            task.run();
                            queue.poll();
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                }
            }

        });
        t.start();
    }


}

public class Demo38 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        },1000);

    }
}

运行结果为:

该代码的运行逻辑:

  1. 创建MyTimer类定时器,执行其构造方法,创建线程t后阻塞
  2. 定时器对象timer向队列中提交任务,优先级队列按照等待时间的大小排序
  3. 由于使用schedule方法,线程t被唤醒,取得队列最首元素将其执行时间与当前时间比较,如果执行时间小于当前时间,线程t仍然会被阻塞(再进行schedule操作时唤醒),否则,任务会被分派到线程上面执行

注意:

  1. 主线程(提交任务)和t线程(执行任务)的相互唤醒
  2. t线程的作用:Timer 类中的存在一个 工作 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务
相关推荐
froginwe111 小时前
Shell test 命令详解
开发语言
沐知全栈开发2 小时前
jQuery 密码验证
开发语言
破晓之翼2 小时前
金蝶EAS OpenAPI 开发说明文档
java·经验分享·其他
空空潍2 小时前
Redis点评实战篇-关注推送
java·数据库·redis·缓存
季明洵2 小时前
Java实现循环队列、栈实现队列、队列实现栈
java·数据结构·算法··队列
Tisfy2 小时前
Windows - VsCode导致Windows凭据过多之一键删除
ide·windows·vscode
学编程的闹钟2 小时前
安装GmSSL3库后用VS编译CMake源码
c语言·c++·ide·开发工具·cmake·visual studio
一 乐2 小时前
英语学习平台系统|基于springboot + vue英语学习平台系统(源码+数据库+文档)
java·vue.js·spring boot·学习·论文·毕设·英语学习平台系统
宇木灵10 小时前
C语言基础学习-二、运算符
c语言·开发语言·学习