volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器

本文详细探讨了Java中的volatile关键字如何保证内存可见性,以及它与synchronized的区别。同时,解释了wait和notify在多线程协作中的作用,以及它们与sleep方法的不同。此外,还介绍了单例模式的饿汉和懒汉实现,以及在多线程环境下的线程安全问题。
摘要由CSDN通过智能技术生成

目录

可见性

volatile

volatile保证内存可见性

volatile不保证原子性

synchronized也可以保证内存可见性 

wait和notify

wait ()

notify()

notifyAll()

wait和sleep对比

顺序执行ABC三个线程

单例模式

饿汉模式

懒汉模式

懒汉模式和饿汉模式在多线程环境下调用getInstance,是否线程安全?

如何让懒汉模式线程安全?

模拟阻塞队列中的put和take方法

 生产者消费者模型

定时器

定时器会使用在哪些场景

标准库中的定时器


可见性

一个线程对共享变量的修改,可以及时的被其他线程看到。

从JMM角度表述内存可见性问题:

JMM:JAVA MEMORY MODEL内存模型
1.线程之间的共享变量存在 主内存 (Main Memory).
2.每一个线程都有自己的 " 工作内存 " (Working Memory) .
3.当线程要读取一个共享变量的时候 , 会先把变量从主内存拷贝到工作内存 , 再从工作内存读取数据 .
4.当线程要修改一个共享变量的时候 , 也会先修改工作内存中的副本 , 再同步回主内存 .
5.当t1线程进行读取,t2线程进行修改的时候,先修改工作内存的值,然后再把工作内存的内容同步到主内存中,但是由于编译器的优化,导致t1没有重新从工作内存同步到主内存,读到的结果就是修改之前的结果,
工作内存=CPU寄存器+CPU的缓存cache; (CPU读取寄存器比读取内存快得多,因此会在CPU内部引入缓存cache,有的CPU可能没有cache,有的有1个,有的是多个,现在普遍是三级)

volatile

简要介绍 

`volatile`是Java中的关键字,用于声明一个变量在多线程环境下具有可见性(visibility)和有序性(ordering)特性。在Java内存模型中,每个线程都有自己的工作内存,而`volatile`关键字可以确保当一个线程修改了共享变量的值时,其他线程能够立即看到这个变化

具体来说,使用`volatile`关键字修饰的变量,其读取和写入操作都会直接访问主内存,而不是访问工作内存。这意味着:

1. 当一个线程对一个`volatile`变量进行写操作时,JVM会立即把该变量的新值刷新到主内存中,而不是先缓存在工作内存中。
2. 当一个线程对一个`volatile`变量进行读操作时,JVM会从主内存中获取该变量的值,而不是从工作内存中获取。

因此,`volatile`变量的修改对于其他线程是可见的,并且修改操作之间的顺序是有序的。但是,`volatile`仅仅保证了可见性和有序性,并不能保证原子性。如果需要保证原子性,需要使用`synchronized`关键字或者`java.util.concurrent.atomic`包中提供的原子类。

volatile保证内存可见性禁止指令重排序

计算机中运行的程序通常要访问数据,而数据通常在内存中,新定义的变量也存储在内存中,而计算机的内存相对于寄存器的访问/读写速度要慢的多,为了提高效率,编译器就可能对代码进行优化,把一些本来要读取内存(主内存)的操作,优化成读取寄存器(工作内存),减少读取内存的次数,整体提高程序效率。本质上就是编译器的优化,但是在多线程环境下,他就有可能会出错,这种问题就叫做 ‘内存可见性’ 问题,有时候在cmp等非load操作里加上Thread.sleep(1000)之类就可能会不触发编译器优化。由于神魔时候会触发内存可见性问题,说不上来,所以使用volatile修饰是更靠谱的选择。

示例:当t2线程输入非零值,t1线程结束

public class demo8 {
    public static int isQuit=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while(isQuit==0){

            }
            System.out.println("👻t1线程结束啦");
        });
        t1.start();
        Thread t2=new Thread(()->{
            Scanner scan=new Scanner(System.in); System.out.println("输入isQuit的值:");
            isQuit=scan.nextInt();

        });
        t2.start();
    }
}

观察运行结果:t1并没有结束,通过工具查看t1仍然处于Runnable状态。

这里就是内存可见性(线程安全)问题:load操作是将内存中的isQuit读取到寄存器中,触摸屏操作室比较寄存器中isQuit是否是0,决定循环是否要结束。这个循环的速度极快,短时间会进行大量的load和cmp,而一次load花的时间相当于上万次cmp,编译器就进行优化,改为最开始进行一次load,后续全是cmp,即只在第一次循环的时候读取内存,后续都是从寄存器中取出isQuit的值。这必然会有bug,编译器的错误判断。



代码在写入 volatile 修饰的变量的时候,
改变线程工作内存中 volatile 变量副本的值, 将改变后的副本的值从工作内存刷新到主内存;
代码在读取 volatile 修饰的变量的时候,
从主内存中读取 volatile 变量的最新值到线程的工作内存中, 从工作内存中读取 volatile 变量的副本;

前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况. 
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.
 

✅由此可见:

内存可见性问题,一个线程针对一个变量进行读取操作,同时另一个线程针对变量进行修改,此时读到的值不一定是修改之后的值。这个读线程没有感知到变量的变化

归根结底就是:jvm或者编译器在多线程优化环境下产生了误判

此时就需要我们手动给flag变量添加volatile关键字(告诉编译器这个变量是易变的,每一次一定要重新读取这个变量的内存内容,指不定啥时候就变了,不能再进行激进的优化了

编译器是否会优化是个玄学问题,我们最好加上volatile.

✅上述内存可见性 编译器优化 的问题,也并不是始终都会出现的,编译器只是可能会误判

✅其他知识:方法中的变量是存放在栈中的,每一个线程都有自己的内存空间,即使是同一个方法,被不同的线程掉用,这里的局部变量还是会处在不同的栈空间上,本质上还是不通的变量。 

volatile不保证原子性(本质区别)

volatile synchronized 有着本质的区别 . synchronized 能够保证原子性 , volatile 保证的是内存可见 性.
volatile不能处理并发++的情况;
public class VolatileRaceConditionExample {
    private static volatile int counter = 0;
    private static final int N = 1000000;

    public static void main(String[] args) {
        Thread incrementThread1 = new Thread(() -> {
            for (int i = 0; i < N; i++) {
                counter++; // 自增操作,由于不是原子操作,可能导致竞态条件
            }
        });

        Thread incrementThread2 = new Thread(() -> {
            for (int i = 0; i < N; i++) {
                counter++; // 自增操作,由于不是原子操作,可能导致竞态条件
            }
        });

        incrementThread1.start();
        incrementThread2.start();

        try {
            incrementThread1.join();
            incrementThread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + counter);
    }
}

两个线程incrementThread1incrementThread2同时对volatile int类型的counter变量进行自增操作。由于自增操作实际上包括读取、修改、写入三个步骤,并且这些步骤之间没有加锁或同步机制,因此可能导致竞态条件,最终结果可能不符合预期。

synchronized也可以保证内存可见性 

”synchronized 既能保证原子性 , 也能保证内存可见性“,这个观点存疑,没有办法用代码验证。线程的最大问题就是抢占式执行,随机性调度,我们需要控制线程之间的执行顺序,虽然线程在内核的调度都是随机的,但是可以通过一些api让线程主动阻塞,主动放弃cpu。ynchronized 除了保证互斥访问外,还可以确保共享变量的可见性, 当一个线程获取了锁并修改了共享变量的值,其他线程在获取锁时会重新读取该变量的值,从而确保了可见性。而volatile是保证一个线程修改变量的值,其他线程能立即看到最新的值,因为每次访问volatile变量都会从主内存重新读取该变量的值,从而保证可见性。
  • synchronized 是可重入的,即线程可以重复获取同一个锁。ReentrantLock 也支持可重入性
  • synchronized 是非公平锁,不能保证等待时间最长的线程优先获得锁。
  • 其他锁比如ReentranLock可以选择支持公平性,即按照等待时间的先后顺序来获取锁。提供lock和unlock方法,可中断的锁,非阻塞的获取锁。
  • 其他锁通常提供了 Condition 接口,可以实现更灵活的线程间协作。
  • synchronized 通过 wait()、notify() 和 notifyAll() 等方法实现线程间的协作
  • volatile 适用于单个变量的读写操作,并且变量之间没有依赖关系。(轻量级)
  • synchronized 更适用于对一段复杂代码块或方法进行同步控制,可以保证一段代码的原子性和可见性。(重量级,涉及到获取锁释放锁)
  • volatile 不能保证操作的原子性,即不能确保复合操作的完整性,例如 volatile int a = 0; a++; 这样的操作并不能保证是原子的。
  • synchronized 能够保证代码块或方法内的操作是原子的,即同一时刻只有一个线程能够执行 synchronized 代码块或方法.

wait和notify

synchronized 通过 wait()、notify() 和 notifyAll() 等方法实现线程间的协作。wait,notify可以避免线程饿死。在多线程编程中,线程饿死指的是一个或多个线程由于等待某个条件而无法继续执行的情况。

都是Object对象的方法,eg:t1,t2,两个线程,希望t1先干活,干的差不多了,再让t2干,就可以让t2先wait(阻塞,主动放弃cpu),等t1干的差不多了,再通过notify通知t2,唤醒t2,让t2继续干。

上述场景使用join()的话,只能是t1 100%执行完,t2才开始执行

wait ()

调用wait方法,就会进入阻塞状态,进入waiting状态,这个可以通过interrupt异常唤醒;

wait()方法中不添加任何的参数,就是死等,除非有别的线程唤醒她。

wait 结束等待的条件 :
其他线程调用该对象的 notify 方法 .
wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本 , 来指定等待时间 ).
其他线程调用该等待线程的 interrupted 方法 , 导致 wait 抛出 InterruptedException 异常 .

为什么有这个异常 ?

要理解wait操作是干啥:

1.先释放锁; 

2.进行阻塞等待;

3.收到通知后,重新尝试获取锁,并且在获取锁之后,继续向下执行。

就好比是单身(没被加锁)还想着分手(就想释放锁)的事情,wait的等不会占用cpu

所以wait要搭配synchronized来使用

public class demo44 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        synchronized(object){
            System.out.println("wait之前");
            object.wait();
            System.out.println("wait之后");
        }
    }
}

此时就只会打印出“wait之前” 

这里虽然wait是阻塞了,阻塞在synchronized代码块里,实际上,这里的阻塞是释放了锁的,此时其他线程是可以获取到object这个对象的锁的~此时这里的阻塞,就处于waiting状态

wait()无参数版本就是死等的;

wait()带参数版本,是指定了等待的最大时间;

HH 

notify()

唤醒当前对象上阻塞的线程,wait是让指定线程阻塞,join只能控制线程结束的先后顺序。 

public class demo99 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        Thread t1=new Thread(()->{
            //这个线程负责等待;
            System.out.println("t1:wait之前");
            synchronized(object){
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t2:wait之后");

        });
        Thread t2=new Thread(()->{
            System.out.println("t2notify之前");
            synchronized(object){
//notify必须获取到锁才能进行通知;
                object.notify();
            }
            System.out.println("t2:notify之后");

        });
        t1.start();
        Thread.sleep(500);
        t2.start();
    }

}

在代码的最后

写t1.start();t2.start();由于线程调度的不确定性,此时不能保证先执行wait,后执行notify,如果先调用notify,此时没有人wait,此处的wait没法被唤醒的,但是也没啥副作用

notifyAll()

notify 方法只是唤醒某一个等待线程 . 使用 notifyAll 方法可以一次唤醒所有的等待线程 .

wait和sleep对比

wait的待有时间参数的版本看起来和sleep有点像,其实有本质区别的,虽然都是指定等待时间,虽然也都能指定等待时间,虽然也能被提前唤醒,(wait是使用notify唤醒,sleep使用interrupted唤醒)

notify唤醒wait,不会有任何异常;

interrupt唤醒sleep则是出异常了。

其实理论上 wait sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻 塞一段时间,
唯一的相同点就是都可以让线程放弃执行一段时间 .
1. wait 需要搭配 synchronized 使用 . sleep 不需要 .
2. wait Object 的方法  ,sleep Thread 的静态方法 .

顺序执行ABC三个线程

public class demo999 {
    public static void main(String[] args) {
        Object locker1=new Object();
        Object locker2=new Object();
        Thread t1=new Thread(()->{
            System.out.println("A");
            synchronized(locker1){
                locker1.notify();
            }
        });
        Thread t2=new Thread(()->{
            synchronized(locker1){
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("B");
            synchronized(locker2){
                locker2.notify();
            }
        });
        Thread t3=new Thread(()->{
            synchronized(locker2){
                try {
                    locker2.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

            System.out.println("C");
        });
        t2.start();
        t3.start();
        t1.start();
        /***
         * 如果程序先执行t1的notify,后执行t2的wait
         *就僵住了
         *
         * **/
    }

}

单例模式

介绍 

有些情况下,需要确保一个类只有一个实例存在,例如线程池、日志记录器、配置管理器等。单例模式可以确保在应用程序中只存在一个全局唯一的实例,方便统一管理和访问。并且单例模式节省资源, 要求只能new一次,由编译器要求

* 在类内部提供了一个现成的实例;
* 把构造方法设置成private,避免其他代码创建实例
文本编辑器.(记事本)
比如需要打开一个非常大的文件 (10 GB)
1.先把所有的内容,都加载到内存中,然后再显示内容.(加载过程会很慢)2.只加载一小部分数据到内存,立即显示内容,随着用户翻页,再动态加载其他内容.[懒汉]
⛅单例模式能保证某个类在程序中只存在唯一一份实例(对象), 而不会创建出多个实例.
⛅单例模式有很多种,这里介绍 懒汉模式 和 饿汉模式 (饿汉模式;类加载阶段就把实例创建出来了,类加载时比较靠前的  )。SHEJIMOSHI

饿汉模式

class Singleton{
    private static Singleton instanse=new Singleton();
    /**
     * Singleton这个属性与实例无关,而是与类有关,java代码中的每一个类,都会在编译完成后得到.class文件,、
     * JVM运行时就会加载这个.class文件读取其中的二进制文件,并且在内存中构造出对应的类对象
     * 由于类对象在Java进程中只有唯一一份,因为类对象内部的类属性也是唯一一份;
     * **/
  
    public static Singleton getInstance(){
        return instanse;
    }
    //为了避免Singleton被复制出来多份
    //把构造方法设为private,在类外就无法通过new 的方式获取Singleton对象了;
    private Singleton(){

    }
}
public class demo110 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1==s2);
    }

}
1.此时打印结果就是true;
2.在这里第二行的static是必要的
(1)static保证这个实例唯一;
(2)static保证这个实例在一定时间被创建出来
3.如何保证类对象唯一?
(1)static 这个操作是让当前的instanse对象变成 类属性,
类属性是长在类对象上的,类对象又是唯一实例的,只是在类加载的时候被创建一个实例。
(2)构造方法设为private,在类外是无法new的。                                                                 
4.类加载模式是啥?

  要执行Java程序的前提就是让类先加载出来。 

 类对象本身与类属性无关,仅仅是因为类里面使用static修饰变量,会作为类属性,也就相当于这个属性对应的内存空间在类对象里面。static与类有关,与实例无关

懒汉模式

class SingletonLazy{
    private static SingletonLazy instance=null;
    public static SingletonLazy getInstance(){
        if(instance==null){
            instance=new SingletonLazy();
            //这个实例并不是类加载的时候就创建,而是在第一次使用的时候才去创建,
            //如果不用,就不创建了
        }
         return instance;

    }
    private SingletonLazy(){};

}
public class demo1000 {
    public static void main(String[] args) {
        SingletonLazy s1=SingletonLazy.getInstance();
        SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s1==s2);
    }
}

懒汉模式和饿汉模式在多线程环境下调用getInstance,是否线程安全?

饿汉模式里的getInstance里的操作只涉及到了读的操作;懒汉既有读也有写 ;懒汉并不是线程安全的,可能会进行多次new操作

如果多个线程同时修改同一个变量,会线程安全问题;

如果多个线程同时读取同一个变量,不会线程安全,懒汉模式就涉及到读取和修改,就可能存在线程安全问题。

如何让懒汉模式线程安全?

加锁

 但是此时还是有问题,内存可见性问题:

加入有很多线程,同时进行getInstance操作,这个时候,是否还会有被优化的风险(只有第一次读使读内存,之后都是读寄存器/cache)。

new操作可能涉及指令重排序问题;

instance=new SingletonLazy()操作可以分成三个步骤:

1.申请内存空间;

2.调用构造方法,把这个内存空初始化成一个合理的对象;

3.把内存空间的地址赋值给instance引用;

正常情况下就是根据123执行,但是编译器还有一手,指令重排序~为了提高程序效率,调整代码执行顺序,顺序就有可能被调整。如123就可能变成132,如果是单线程,123和132就没有本质区别。

但是在多线程情况下,t1线程是按照132的顺序执行,t1执行完13后,执行2的时候被切出cpu由t2来执行,t2拿到的就是一个空对象非法的对象,还没构造完的不完整对象。

解决办法就是加volatile

1.解决内存可见性;

2.解决指令重排序问题;

class SingletonLazy1{
    public volatile static SingletonLazy1 instance = null;
    public static SingletonLazy1 getInstance(){
        if (instance == null) {
            synchronized(SingletonLazy1.class){
                if(instance==null){
                    instance=new SingletonLazy1();
                }
            }
        }
        return instance;
    }
    private SingletonLazy1(){};

}
//如果有了对象就线程安全,不用加锁,是否加锁
//判断是否现需要new

 加锁不是说线程就赖在CPU上不走了,而是切换调度正常,但是其他线程尝试枷锁就阻塞;

这里加锁的作用是让原本不是原子的操作变成原子性操作;并且加锁是解决线程安全问题的关键,加锁必须是两个线程同时对同一个线程加锁才会产生线程阻塞;

模拟阻塞队列中的put和take方法

这里是普通队列

 public void put(int value){
        if(size==items.length){
            //队列已经满了
            return;
        }
        items[tail]=value;
        tail++;
        //记得对tail的处理
        //第(1)种写法
        tail=tail%items.length;
        //第(2)中写法
        if(tail>=items.length){
            tail=0;
        }
    }
    //出队列
    public Integer take(){
        if(size==0){
            return null;
        }
        int result=items[head];
        head++;
        if(head>=items.length){
            head=0;
        }
        size--;
        return result;
    }

/**自己写阻塞队列;
 * 此处不考虑泛型,直接使用int代替
 * */
/***
 * 普通队列的实现加上阻塞功能就变成了阻塞队列,是在多线程环境下的阻塞;
 * 加上synchronized包裹整个方法体
 * */
/**
 * 但是有一个问题:
 * 如果他们两个线程的wait同时触发了;
 * 那显然就不能在正确的唤醒了;
 *
 *
 * **/
class MyBlockingQueue{
    private int[] items=new int[200];
    private int head=0;
    private int tail=0;
    private int size=0;
    //入队列
    public void put(int value) throws InterruptedException {
        synchronized(this){
            while(size==items.length){
                //队列已经满了,再放元素,就会产生阻塞
                this.wait();
            }
            items[tail]=value;
            tail++;
            //记得对tail的处理
            //第(1)种写法
            tail=tail%items.length;
            //第(2)中写法
            if(tail>=items.length){
                tail=0;
            }
            this.notify();//唤醒take()里面的wait;
        }

    }
    //出队列
    public Integer take() throws InterruptedException {
        int result=0;
        synchronized (this){
            if(size==0){//队列为空,还要求出队列,出现阻塞;
              this.wait();
            }
            result=items[head];
            head++;
            if(head>=items.length){
                head=0;
            }
            size--;
            this.notify();//唤醒take()里面的阻塞等待;
        }
        return result;

    }

}

 生产者消费者模型

public static void main(String[] args) {
        MyBlockingQueue queue=new MyBlockingQueue();
        Thread customer=new Thread(()->{
            while(true){
                try {
                    int result=queue.take();
                    System.out.println("消费: "+result);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        });
        Thread producer=new Thread(()->{
            int count=0;
            while(true){
                try {
                    System.out.println("生产 "+ count );
                    queue.put(count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
            }
        });




    }

工厂模式

定时器

定时器会使用在哪些场景

定时器是一种实际开发中非常常用的组件.
比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.
比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).
类似于这样的场景就需要用到定时器.

标准库中的定时器

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);
/**
*标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
*schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 
*第二个参数指定多长时间之后执行 (单位为毫秒).
TimerTask继承Runnable接口
*/

主线程执行 schedule 方法的时候,就是把这个任务给放到 timer 对象中了,同时,timer 里头也包含一个线程,这个线程叫做"扫描线程”,一旦时间到,扫描线程就会执行刚才安排的任务了.

这个程序并没有结束,Timer中的线程阻止了。

定时器的实现 

1.  timer内部搞扫描线程,扫描任务是否到时间,到时间就执行。扫描线程也只用扫描优先级队列的第一个元素。

2.一个定时器可以注册N个任务,N个任务会按照最初约定的任务,按照顺序执行;

优先级队列

此处的优先级队列会在多线程环境下使用,使用schedule是一个队列,扫描线程是另一个的队列;

定时器的构成:

1.一个带优先级的阻塞队列
为啥要带优先级呢 ?
因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的 . 使用带 优先级的队列就可以高效的把这个 delay 最小的任务找出来 .
2.队列中的每个元素是一个 Task 对象 .
3.Task 中带有一个时间属性.
4. 同时有一个 worker 线程一直扫描队首元素 , 看队首元素是否需要执行。

Timer中有一个线程,扫描任务是否到时间可以执行;一个数据结构保存所有任务;一个类通过类的对象描述一个任务包含时间和内容

定时器
1.类,描述一个任务
2.优先级队列,组织所有的任务

3.扫描线程,负责验证队首元素是否能执行

Task实现 

static class Task implements Comparable<Task> {
        private Runnable command;
        private long time;
        public Task(Runnable command, long time) {
            this.command = command;
            // time 中存的是绝对时间, 超过这个时间的任务就应该被执行
            this.time = System.currentTimeMillis() + time;
       }
        public void run() {
            command.run();
       }
        @Override
        public int compareTo(Task o) {
            // 谁的时间小谁排前面
            return (int)(time - o.time);
       }
   }
}
Task 类用于描述一个任务 ( 作为 Timer 的内部类 ). 里面包含一个 Runnable 对象和一个 time( 毫秒时 间戳)
这个对象需要放到 优先队列 中. 因此需要实现 Comparable 接口.

 完整代码

public class Timer {
    static class Task implements Comparable<Task> {
        private Runnable command;
        private long time;
        public Task(Runnable command, long time) {
            this.command = command;
            // time 中存的是绝对时间, 超过这个时间的任务就应该被执行
            this.time = System.currentTimeMillis() + time;
       }
        public void run() {
            command.run();
       }

        @Override
        public int compareTo(Task o) {
            // 谁的时间小谁排前面
            return (int)(time - o.time);
       }
   }
    // 核心结构
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();
    // 存在的意义是避免 worker 线程出现忙等的情况
    private Object mailBox = new Object();
    class Worker extends Thread{
        @Override
        public void run() {
            while (true) {
                try {
                    Task task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime) {
                        // 时间还没到, 就把任务再塞回去
                        queue.put(task);
                        synchronized (mailBox) {
                            // 指定等待时间 wait
                            mailBox.wait(task.time - curTime);
                       }
                   } else {
                        // 时间到了, 可以执行任务
                        task.run();
                   }
               } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
               }
           }
       }
   }
    public Timer() {
        // 启动 worker 线程
        Worker worker = new Worker();
        worker.start();
   }
    // schedule 原意为 "安排"
    public void schedule(Runnable command, long after) {
        Task task = new Task(command, after);
        queue.offer(task);
        synchronized (mailBox) {
            mailBox.notify();
       }
   }
// Timer 实例中, 通过 PriorityBlockingQueue 来组织若干个 Task 对象. 
//通过 schedule 来往队列中插入一个个 Task 对象.
    public static void main(String[] args) {
        Timer timer = new Timer();
        Runnable command = new Runnable() {
            @Override
            public void run() {

                System.out.println("我来了");
                timer.schedule(this, 3000);
           }
       };
        timer.schedule(command, 3000);
   }
}
/***
 * 当时间没到要执行的时候, CPU就把线程拿起来又放下,进行忙等,
 * 所以我们选择“阻塞时等待”,使用wait,更方便唤醒,
 * 使用wait等待,更方便随时唤醒,使用wait等待每次有新任务来了(有人调用schedule)
 * 就重新检查时间,重新计算要等待的时间,并且wait也停工了一个带有:超过时间的版本
 * */
/**
 *堆的take:出堆顶元素 ;底层操作
 * 交换堆顶元素和最后一个元素,进行向下调整
 *堆的put:入,
 * 先放在最后一个元素的位置,然后进行向上调整 
 * */

一个极端情况

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sqyaa.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值