多线程多线程案例

Posted bit me

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程多线程案例相关的知识,希望对你有一定的参考价值。

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇
✨每日一语:we can not judge the value of a moment until it becomes a memory.

目 录


🍝一. 单例模式

单例模式是校招中最常考的设计模式之一.

啥是设计模式?

设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.
 
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.

单例模式目的:有些对象,在一个程序中应该只有唯一一个实例,就可以使用单例模式。单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。

一个程序中应该只有唯一一个实例是程序猿来保证的,不一定靠谱,于是在单例模式下借助语法,强行限制咱们不能创建多个实例。

Java 里的单例模式,有很多种实现方式,主要介绍两个大类:饿汉模式懒汉模式

饿汉模式:程序启动,则立即创建实例

懒汉模式:程序启动,先不着急创建实例,等到真正用的时候,再创建


单例模式具体实现方式:

🍤1. 饿汉模式实现

class Singleton
    private static Singleton instance = new Singleton();

    public static Singleton getInstance()
         return instance;
    

    //构造方法设为私有!其他的类想来 new 就不行了
    private Singleton() 


public class Demo19 
    public static void main(String[] args) 
        Singleton instance = Singleton.getInstance();
        Singleton isstance2 = Singleton.getInstance();
        System.out.println(instance == isstance2);
    

  1. static 静态,实际效果和字面意思没有任何关联,实际的含义是:类属性 / 类方法。
  2. 类属性就长在类对象上,类对象在整个程序中只有唯一一个实例!!(JVM 保证的)
  3. 在静态方法中,后续我们需要使用这个实例,统一基于 getInstance 方法来获取,实例独苗,不需要再 new 了(new 也会失败)
  4. 使用静态成员表示实例(唯一性) + 让构造方法为私有(堵住了 new 创建新实例的口子)

按照现在的代码,当 Singleton 类被加载的时候,就会执行到此处的实例化操作!!实例化时机非常早!(非常迫切的感觉)


🦪2. 懒汉模式实现

class Singletonlazy
    private static Singletonlazy instance = null;

    public static Singletonlazy getInstance()
        if(instance == null)
            instance = new Singletonlazy();
        
        return instance;
    

    private Singletonlazy() 

  1. 第一步并没有创建实例!
  2. 首次调用 getInstance 才会创建实例!

上面俩种模式还涉及到线程安全。

  • 饿汉模式是线程安全的,多线程涉及 getInstance ,只是多线程读,没事儿
  • 懒汉模式是线程不安全的,有的地方在读,有的地方在写,一旦实例创建好了之后,后续 if 条件就进不去了,此时也就全是读操作了,也就线程安全了。

如何解决懒汉模式线程不安全?

方法就是加锁:

synchronized (Singletonlazy.class) 
    if (instance == null) 
        instance = new Singletonlazy();
    

把读和写两个步骤打包在一起,保证读 判定 修改 这组操作是原子的!

懒汉模式,只是初始情况下,才会有线程不安全问题,一旦实例创建好了之后,此时就安全了!既然如此,后续在调用 getlnstance 的时候就不应该再尝试加锁了!当线程安全之后,再尝试加锁,就非常影响效率了。

如上代码我们只需要再嵌套一个 if 判定即可

public static Singletonlazy getInstance()
    if (instance == null) 
        synchronized (Singletonlazy.class) 
            if (instance == null) 
                instance = new Singletonlazy();
            
        
    
    return instance;

注意!不要用单线程的理解方式来看待多线程代码!如果是单线程,连续两个一样的 if 判定,毫无意义!但是多线程就不是了,尤其是中间隔了个加锁操作!

  • 加锁操作可能就涉及到阻塞,前面的 if 和后面的 if 中间可能就隔了个 “沧海桑田”。
  • 外层 if 判定当前是否已经初始化好,如果未初始化好,就尝试加锁,如果是已初始化好,那么就直接往下走。
  • 里层的 if 是在多个线程尝试初始化,产生了锁竞争,这些参与锁竞争的线程,拿到锁之后,再进一步确认,是否真的要初始化。

理解双重 if 判定:最核心的目标,就是降低锁竞争的概率
 
当多线程首次调用 getInstance,发现 instance 为 null, 于是又继续往下执行来竞争锁,其中竞争成功的线程, 再完成创建实例的操作。当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例

  1. 有三个线程, 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没有创建的消息. 于是开始竞争同一把锁.
  2. 其中线程1 率先获取到锁, 此时线程1 通过里层的 if (instance == null) 进一步确认实例是否已经创建. 如果没创建, 就把这个实例创建出来.
  3. 当线程1 释放锁之后, 线程2 和 线程3 也拿到锁, 也通过里层的 if (instance == null) 来确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了.
  4. 后续的线程, 不必加锁, 直接就通过外层 if (instance == null) 就知道实例已经创建了, 从而不再尝试获取锁了. 降低了开销.

很多线程尝试读,这样的读,是否会被优化成读寄存器呢?

第一个线程读,把内存的数据读到寄存器了,第二个线程也去读,会不会就直接重复利用上述寄存器的结果呢?由于每个线程有自己的上下文,每个线程有自己的寄存器内容,因此按理来说是不会出现优化的,但是实际上不一定,因此在这个场景下,给 instance 加上 volatile 是最稳健的做法!

volatile private static Singletonlazy instance = null;

对懒汉模式的总结:

  • 加锁
  • 双重 if 判定(外层 if 为了降低加锁的频率,降低锁冲突的概率,里层 if 才是真正判定是否要实例化)
  • volatile

🍲二. 阻塞式队列

阻塞队列是什么?

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

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

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
  • 阻塞队列:能够保证 “线程安全”
  • 无锁队列:也是一种线程安全的队列,实现内部没有使用锁,更高效,消耗更多的 CPU 资源。
  • 消息队列:在队列中涵盖多种不同 “类型” 元素,取元素的时候可以按照某个类型来取,做到针对该类型的 “先进先出” (甚至说会把消息队列作为服务器,单独部署)

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


生产者消费者模型

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

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

优点:

  1. 可以更好地做到 “解耦合”。


A 直接给 B 发送数据,就是耦合性比较强,开发 A 的时候就得考虑 B 是如何接收的,开发 B 的时候就得考虑 A 是如何发送的。极端情况下 A 出现问题挂了 可以能也造成 B 出现问题导致 B 也挂了,反之 B 出现了问题,也会牵连 A 导致 A 挂了。
 
于是在阻塞队列的影响下,A 和 B 不再直接交互
开发阶段:A 只用考虑自己和队列如何交互,B 也只用考虑自己和队列如何交互,A 和 B 之间都不需要知道对方的存在。
部署阶段:A 如果挂了,对 B 没有任何影响;B 如果挂了,对 A 没有任何影响。

  1. 能够做到 “削峰填谷” ,提高整个系统抗风险能力。


程序猿无法控制外网有多少个用户在访问 A,当出现极端情况,外网访问请求大量涌入的时候,A 把所有请求的数据一并转让给 B 的时候,B 就容易扛不住而挂掉。
 
在阻塞队列的影响下
多出来的压力队列承担了,队列里多存一会儿数据就行了,即使 A 的压力比较大,B 仍按照固定的频率来取数据。


标准库中的阻塞队列

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

  • BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
  • put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
  • BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

生产者消费者模型:

public class Demo20 
    public static void main(String[] args) 
        BlockingDeque<Integer> queue = new LinkedBlockingDeque<>();

        Thread customer = new Thread(()->
           while (true)
               try 
                   int value = queue.take();
                   System.out.println("消费元素:" + value);
                catch (InterruptedException e) 
                   e.printStackTrace();
               
           
        );
        customer.start();

        Thread producer = new Thread(()->
            int n = 0;
            while (true)
                try 
                    System.out.println("生产元素:" + n);
                    queue.put(n);
                    n++;
                    Thread.sleep(500);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );
        producer.start();
    

运行结果演示图:

阻塞队列实现:

  • 自己模拟实现一个阻塞队列
  • 基于数组的方式来实现队列
  • 两个核心方法:1. put 入队列; 2. take 出队列
class MyBlockingQueue 
    // 假定最大是 1000 个元素,当然也可以设定成可配置的
    private int[] items = new int[1000];
    //队首的位置
    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++;
            if (tail == items.length) 
                //注意 如果 tail 到达数组末尾,就需要从头开始
                tail = 0;
            
            size++;
            //即使没人在等待,多调用几次 notify 也没事,没负面影响
            this.notify();
        
    

    //出队列
    public Integer take() throws InterruptedException 
        int ret = 0;
        synchronized (this) 
            while (size == 0) 
                //队列为空,就等待
                this.wait();
            
            ret = items[head];
            head++;
            if (head == items.length) 
                head = 0;
            
            size--;
            this.notify();
        
        return ret;
    


public class Demo21 
    public static void main(String[] args) throws InterruptedException 
        MyBlockingQueue queue = new MyBlockingQueue();
        queue.put(100);
        queue.take();
    

  • 入队列中的 wait出队列中的 notify 对应,满了之后,入队列就要阻塞等待,此时在取走元素之后,就可以尝试唤醒了。
  • 入队列中的 notify出队列中的 wait 对应,队列为空,也要阻塞,此时在插入成功之后,队列就不为空了,就能够把 take 的等待唤醒。
  • 一个线程中无法做到又等待又唤醒
  • 阻塞之后,就要唤醒,阻塞和唤醒之间是沧海桑田,虽然按照当下代码是有元素插入成功了,条件不成立,等待结束。但是更稳妥的做法是把 if 换成 while ,在唤醒之后,再判断一次条件!万一条件又成立了呢?万一接下来要继续阻塞等待呢?

测试代码:

public class Demo21 
    public static void main(String[] args) 
        MyBlockingQueue queue = new MyBlockingQueue();
        Thread customer = new Thread(()->
           while (true)
               int value = 0;
               try 
                   value = queue.take();
                   System.out.println("消费:" + value);
                   Thread.sleep(500);
                catch (InterruptedException e) 
                   e.printStackTrace();
               
           
        );
        customer.start();

        Thread producer = new Thread(()->
           int value = 0;
           while (true)
               try 
                   queue.put(value);
                   System.out.println("生产:" + value);
                   value++;
                catch (InterruptedException e) 
                   e.printStackTrace();
               
           
        );
        producer.start();
    

延缓了消费代码,也可以把生产代码延缓,调用 sleep 即可
 

  • 延缓消费代码
  • 延缓生产代码

多线程四大经典案例及java多线程的实现

目录

本节要点

  • 了解一些线程安全的案例
  • 学习线程安全的设计模型
  • 掌握单例模式,阻塞队列,生产在消费者模型

单例模式

我们知道多线程编程,因为线程的随机调度会出现很多线程安全问题! 而我们的java有些大佬针对一些多线程安全问题的应用场景,设计了一些对应的解决方法和案例,就是解决这些问题的一些套路,被称为设计模式,供我们学习和使用!

单例模式是校招最常考的一个设计模式之一!!!

什么是单例模式呢?

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个

单例模式的具体实现方法又分为饿汉懒汉两种!
而这里所说的饿并不是贬义词!
饿汉指的是在创建一个类的时候就将实例创建好!比较急!
懒汉指的是在需要用到实例的时候再去创建实例!比较懒!

饿汉模式

饿汉模式联系实际生活中例子:
就是一个人性子比较急,也许一件事情的期限还有好久,而他却把事情早早干完!

因为我们单例模式只能有一个实例
那如何去保证一个实例呢?
我们会马上想到类中用static修饰的类属性,它只有一份!保证了单例模式的基本条件!

显然生活中这样的人很优秀,但是我们的计算机如果这样却不太好!
因为cpu和内存的空间有限,如果还不需要用到该实例,却创建了实例,那不就增加了内存开销,显然不科学.但事实问题也不大!

class Singleton
    //饿汉模式, static 创建类时,就创建好了类属性的实例!
    //private 这里的instance实例只有一份!!!
    private static Singleton instance = new Singleton();
    //私有的构造方法!保证该实例不能再创建
    private Singleton()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton getInstance() 
        return instance;
    

我们可以看到这里饿汉模式,当多个线程并发时,并没有出现线程不安全问题,因为这里的设计模式只是针对了读操作!!! 而单例模式的更改操作,需要看懒汉模式!

懒汉模式

联系实际中的例子就是.就是这个人比较拖延,有些事情不得不做的时候,他才会去做完!

//懒汉模式(线程不安全版本)
class Singleton1
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton1 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton1()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton1 getInstance() 
        if(instance==null)//需要时再创建实例!
            instance = new Singleton1();
        
        return instance;
    

我们分析一下上述代码,该模式,对singleton进行了修改,而我们知道多线程的修改可能会出现线程不安全问题!
当我们多个线程同时对该变量进行访问时!

我们将该代码的情况分成两种,一种是初始化前要进行读写操作,初始化后只需要进行读操作!

  • instance未初始化化前
    多个线程同时进入getInstance方法!那就会创建很多次instance实例!
    联系之前的变量更改内存cpu的操作:

    显然很多线程进行了无效操作!!!也会触发内存不可见问题!!!
  • instance初始化后,进行的读操作,就像上面的饿汉模式一样,并没有线程安全问题!

我们下面进行多次优化

//优化1
class Singleton2
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() 
        synchronized (Singleton.class) //对读写操作进行加锁!
            if(instance==null)//需要时再创建实例!
                instance = new Singleton2();
            
            return instance;
        
    

我们将Singleton类对象加锁后,显然避免了刚刚的一些线程安全问题!但是出现了新的问题!

  • instance初始化前
    在初始化前,我们很好的将读写操作进行了原子封装,并不会造成线程不安全问题!
  • instance初始化后
    然而初始化后的每次读操作却并不好,当我们多个线程进行多操作时,很多线程就会造成线程阻塞,代码的运行效率极具下降!

我们如何保证,线程安全的情况下又保证读操作不会进行加锁,锁竞争呢?

我们可以间代码的两种情况分别处理!

//优化二
class Singleton2
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() 
        if(instance==null)//如果未初始化就进行加锁操作!
            synchronized (Singleton.class) //对读写操作进行加锁!
                if(instance==null)//需要时再创建实例!
                    instance = new Singleton2();
                
            
        
        //已经初始化后直接读!!!
        return instance;
    

我们看到这里可能会有疑惑,咋为啥要套两个if啊,把里面的if删除不行吗!!!
我们来看删除后的效果:

//删除里层if
class Singleton2
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() 
        if(instance==null)//如果未初始化就进行加锁操作!
            synchronized (Singleton.class) //对读写操作进行加锁!
                    instance = new Singleton2();
            
        
        //已经初始化后直接读!!!
        return instance;
    

在删除里层的if后:
我们发现当有多个线程进行了第一个if判断后,进入的线程中有一个线程锁竞争拿到了锁!而其他线程就在这阻塞等待,直到该锁释放后,又有线程拿到了该锁,而这样也就多次创建了instance实例,显然不可!!!

所以这里的两个if都有自己的作用缺一不可!
第一个if:
判断是否要进行加锁初始化
第二个if:
判断该线程实例是否已经创建!

//最终优化版
class Singleton2
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    //volatile 保证内存可见!!!避免编译器优化!!!
    private static volatile Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() 
        if(instance==null)//如果未初始化就进行加锁操作!
            synchronized (Singleton.class) //对读写操作进行加锁!
	            if(instance==null)
	                instance = new Singleton2();
	            
            
        
        //已经初始化后直接读!!!
        return instance;
    

而我们又发现了一个问题,我们的编译器是会对代码进行优化操作的!如果很多线程对第一个if进行判断,那cpu老是在内存中拿instance的值,就很慢,编译器就不开心了,它就优化直接将该值存在寄存器中,而此操作是否危险,如果有一个线程将该实例创建!那就会导致线程安全问题! 而volatile关键字保证了instanse内存可见性!!!

总结懒汉模式

  • if 外层保证未初始化前加锁,创建实例. 里层if保证实例创建唯一一次
  • synchronized加锁,保证读写原子性
  • volatile保证内存可见性,避免编译器优化

阻塞队列

什么是阻塞队列?
顾名思义是队列的一种!
也符合先进先出的特点!
阻塞队列特点:

当队列为空时,读操作阻塞
当队列为满时,写操作阻塞

阻塞队列一般用在多线程中!并且有很多的应用场景!
最典型的一个应用场景就是生产者消费者模型

生产者消费者模型

我们知道生产者和消费者有着供需关系!
而开发中很多场景都会有这样的供需关系!
比如有两个服务器AB
A是入口服务器直接接受用户的网络请求
B应用服务器对A进行数据提供

在通常情况下如果一个网站的访问量不大,那么AB服务器都能正常使用!
而我们知道,很多网站当很多用户进行同时访问时就可能挂!
我们知道,A入口服务器和B引用服务器此时耦合度较高!
当一个挂了,那么另一个服务器也会出现问题!

而当我们使用生产者消费者模型就很好的解决了上述高度耦合问题!我们在他们中间加入一个阻塞队列即可!


当增加就绪队列后,我们就不用担心AB的耦合!
并且AB进行更改都不会影响到对方! 甚至将改变服务器,对方也无法察觉!
而阻塞队列还保证了,服务器的访问速度,不管用户量多大! 这些数据都会先传入阻塞队列,而阻塞队列如果满,或者空,都会线程阻塞! 也就不存在服务器爆了的问题!!!
也就是起到了削峰填谷的作用!不管访问量一时间多大!就绪队列都可以保证服务器的速度!

标准库中的就绪队列

我们java中提供了一组就绪队列供我们使用!

BlockingQueue


BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
put 方法用于阻塞式的入队列,
take 用于阻塞式的出队列.
BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

//生产着消费者模型
public class Test2 
    public static void main(String[] args) throws InterruptedException 
        //创建一个阻塞队列
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
        Thread customer = new Thread(() -> //消费者
            while (true) 
                try 
                    int value = blockingQueue.take();
                    System.out.println("消费元素: " + value);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        , "消费者");
        customer.start();
        Thread producer = new Thread(() -> //生产者
            Random random = new Random();
            while (true) 
                try 
                    int num = random.nextInt(1000);
                    System.out.println("生产元素: " + num);
                    blockingQueue.put(num);
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        , "生产者");
        producer.start();
        customer.join();
        producer.join();
    

阻塞队列实现

虽然java标准库中提供了阻塞队列,但是我们想自己实现一个阻塞队列!

我们就用循环队列实现吧,使用数组!

//循环队列
class MyblockingQueue
    //阻塞队列
    private int[] data = new int[100];
    //队头
    private int start = 0;
    //队尾
    private int tail = 0;
    //元素个数, 用于判断队列满
    private int size = 0;
    public void put(int x)
        //入队操作
        if(size==data.length)
            //队列满
            return;
        
        data[tail] = x;
        tail++;//入队
        if(tail==data.length)
            //判断是否需要循环回
            tail=0;
        
        size++; //入队成功加1
    
    public Integer take()
        //出队并且获取队头元素
        if(tail==start)
            //队列为空!
            return null;
        
        int ret = data[start]; //获取队头元素
        start++; //出队
        if(start==data.length)
            //判断是否要循环回来
            start = 0;
        
       // start = start % data.length;//不建议可读性不搞,效率也低
        size--;//元素个数减一
        return ret;
    




我们已经创建好了一个循环队列,目前达不到阻塞的效果!
而且当多线程并发时有很多线程不安全问题!
而我们知道想要阻塞,那不得加锁,不然哪来的阻塞!

//阻塞队列
class MyblockingQueue
    //阻塞队列
    private int[] data = new int[100];
    //队头
    private int start = 0;
    //队尾
    private int tail = 0;
    //元素个数, 用于判断队列满
    private int size = 0;
    //锁对象
    Object locker = new Object();

    public void put(int x)
       synchronized (locker)//对该操作加锁
           //入队操作
           if(size==data.length)
               //队列满 阻塞等待!!!直到put操作后notify才会继续执行
               try 
                   locker.wait();
                catch (InterruptedException e) 
                   e.printStackTrace();
               
           
           data[tail] = x;
           tail++;//入队
           if(tail==data.length)
               //判断是否需要循环回
               tail=0;
           
           size++; //入队成功加1
           //入队成功后通知take 如果take阻塞
           locker.notify();//这个操作线程阻塞并没有副作用!
       
    
    public Integer take()
        //出队并且获取队头元素
        synchronized (locker)
            if(size==0)
                //队列为空!阻塞等待 知道队列有元素put就会继续执行该线程
                try 
                    locker.wait();
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
            int ret = data[start]; //获取队头元素
            start++; //出队
            if(start==data.length)
                //判断是否要循环回来
                start = 0;
            
            // start = start % data.length;//不建议可读性不搞,效率也低
            size--;//元素个数减一
            locker.notify();//通知 put 如果put阻塞!
            return ret;
        
    

//测试代码
public class Test3 
    public static void main(String[] args) 
            MyblockingQueue queue = new MyblockingQueue();
            Thread customer = new Thread(()->
                int i = 0;
                while (true)
                    System.out.println("消费了"+queue.take());
                
            );

                    Thread producer = new Thread(()->
                        Random random = new Random();
                        while (true)
                            int x = random.nextInt(100);
                            System.out.println("生产了"+x);
                            queue.put(x);
                            try 
                                Thread.sleep(100);
                             catch (InterruptedException e) 
                                e.printStackTrace();
                            
                        
                    );
                    customer.start();
                    producer.start();
    


可以看到通过waitnotify的配和,我就实现了阻塞队列!!!

定时器

定时器是什么

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

也就是说定时器有像joinsleep等待功能,不过他们是基于系统内部的定时器,
而我们要学习的是在java给我们提供的定时器包装类,用于到了指定时间就执行代码!
并且定时器在我们日常开发中十分常用!

java给我们提供了专门一个定时器的封装类Timerjava.util包下!

Timer定时器

Timer类下有一个schedule方法,用于安排指定的任务和执行时间!
也就达到了定时的效果,如果时间到了,就会执行task!

  • schedule 包含两个参数.
  • 第一个参数指定即将要执行的任务代码,
  • 第二个参数指定多长时间之后执行 (单位为毫秒).
//实例
import java.util.Timer;
import java.util.TimerTask;
public class Demo1 
    public static void main(String[] args) 
        //在java.util.Timer包下
        Timer timer = new Timer();
        //timer.schedule()方法传入需要执行的任务和定时时间
        //Timer内部有专门的线程负责任务的注册,所以不需要start
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("hello Timer!");
            
        ,3000);
        //main线程
        System.out.println("hello main!");
    


我们可以看到我们只需要创建一个Timer对象,然后调用schedule返回,传入你要执行的任务,和定时时间便可完成!

定时器实现

我们居然知道java中定时器的使用,那如何自己实现一个定时器呢!

我们可以通过Timer中的源码,然后进行操作!

Timer内部需要什么东西呢!

我们想想Timer的功能!
可以定时执行任务!(线程)
可以知道任务啥时候执行(时间)
可以将多个任务组织起来对比时间执行

  • 描述任务
    也就是schedule方法中传入的TimerTake
    创建一个专门表示定时器中的任务
class MyTask
    //任务具体要干啥
    private Runnable runnable;
    //任务执行时间,时间戳
    private long time;
    ///delay是一个时间间隔
    public MyTask(Runnable runnable,long delay)
            this.runnable = runnable;
            time = System.currentTimeMillis()+delay;
    
    public void run() //描述任务!
        runnable.run();