【JavaEE】多线程案例——单例模式与阻塞队列

文章目录

  • 一、单例模式
    • 1.饿汉模式
    • 2.懒汉模式
    • 3.线程安全的懒汉模式
  • 二、阻塞队列
    • 1.生产者消费者模型
    • 2.标准库中的阻塞队列
    • 3.生产者消费者模型
    • 4.阻塞队列实现
  • 最后的话

一、单例模式


  什么是单例模式?

单例模式是一种设计模式,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。


  什么是设计模式?

所谓设计模式,简单来说就是一种固定的套路,软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.

  单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种。

  那么,“饿汉模式”与“懒汉模式”的区别是什么呢?

  举个例子,比如说我们在家里吃饭,用了5个碗,吃完之后,立即就把碗洗了,这种模式我们称之为“饿汉模式”。
  如果我们吃完饭用了5个碗之后,不立即洗,然后等到下次再用的时候,比如说我们下次要用两个碗,那么我们就先洗两个碗,需要的时候,我们才去洗碗,这种模式我们称之为“懒汉模式”。


  将上面两个例子放到我们的程序设计当中,则是如下:

  • 饿汉模式:在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。
  • 懒汉模式:当程序第一次访问单件模式实例时才进行创建。

1.饿汉模式

//通过Singleton这个类来实现单例模式,保证Singleton这个类只有唯一实例
class Singleton{
    //1.使用static创建一个实例,并且立即进行实例化
    // 这个instance对应的实例,就是该类的唯一实例
    private static Singleton instance = new Singleton();

    //2.为了防止程序猿在其他地方不小心new这个Singleton,就可以把构造方法设为private
    private Singleton(){}

    //3.提供一个方法,让外面能够拿到唯一实例
    public static Singleton getInstance(){
        return instance;
    }
}

public class Demo19 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
    }
}

【JavaEE】多线程案例——单例模式与阻塞队列_第1张图片

2.懒汉模式

//单例模式-懒汉模式
class Singleton2{
    //1.不需要立即初始化
    public static Singleton2 instance = null;
    //2.把构造方法设为private
    private Singleton2(){}
    //3.提供一个方法来获取以上实例,
    //只有真正需要用到这个实例的时候,才去真正创建这个实例
    public static Singleton2 getInstance(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}

public class Demo20 {
    public static void main(String[] args) {
        Singleton2 instance = Singleton2.getInstance();
    }
}


  我们分析一下上面的“饿汉模式”与“懒汉模式”,看看哪个是线程安全的。

【JavaEE】多线程案例——单例模式与阻塞队列_第2张图片
  
  那么既然上面的“懒汉模式”是不安全的,那么我们如何保证它是安全的呢?
  答案是:加锁。


3.线程安全的懒汉模式

//单例模式-懒汉模式
class Singleton2{
    //1.不需要立即初始化
    public static Singleton2 instance = null;
    //2.把构造方法设为private
    private Singleton2(){}
    //3.提供一个方法来获取以上实例,
    //只有真正需要用到这个实例的时候,才去真正创建这个实例
    public static Singleton2 getInstance(){
        synchronized (Singleton2.class){
            if(instance == null){
                instance = new Singleton2();
            }
        }
        return instance;
    }
}

【JavaEE】多线程案例——单例模式与阻塞队列_第3张图片  
  那么,在上面的框框哪里加了锁就万事大吉了吗?

  实际上,还没有,虽然加锁之后,我们线程安全的问题得到解决了,但是又产生了新的问题。在最初的懒汉模式代码中,它的线程不安全是发生在getInstance被初始化之前的。因为在为初始化的时候,多线程调用getInstance就可能同时涉及到读和修改的操作。

【JavaEE】多线程案例——单例模式与阻塞队列_第4张图片

  
  但一旦instance被初始化之后,getInstance操作就剩下两个读操作了,也就线程安全了

【JavaEE】多线程案例——单例模式与阻塞队列_第5张图片

  
  按照上述的加锁方式,无论是代码初始化之后,还是初始化之前,每次调用getInstance都会进行加锁,也就意味着即便是初始化之后(已经线程安全了),仍会存在大量的锁竞争。

【JavaEE】多线程案例——单例模式与阻塞队列_第6张图片

  那我们该如何优化呢?
  
  让getInstance初始化之前才进行加锁,初始化之后就不再加锁了。
  方式是:在加锁的位置再加一层判断条件——是否已经完成初始化(instance == null)

class Singleton2{
    //1.不需要立即初始化
    public static Singleton2 instance = null;
    //2.把构造方法设为private
    private Singleton2(){}
    //3.提供一个方法来获取以上实例,
    //只有真正需要用到这个实例的时候,才去真正创建这个实例
    public static Singleton2 getInstance(){
//如果这个条件成立,说明当前的单例是未被初始化的,存在线程安全,就需要进行加锁操作
        if(instance == null){
            synchronized (Singleton2.class){
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }

        return instance;
    }
}

  
  实际上,上面的代码还是会存在一些问题的,这里会存在一个内存可见性的问题。如果我们多个线程都去调用这里的getInstance的话,就会造成大量的的读instance内存的操作,这可能会引发编译器的优化,也就是可能会让编译器把这个读内存的操作优化成读寄存器的操作。
  一旦这里进行了优化,后续如果第一个线程已经完成了针对instance的修改,那么紧接着后面的线程就都感知不到这个修改,仍然把instance当成是null.

【JavaEE】多线程案例——单例模式与阻塞队列_第7张图片

  

  这里的解决方案是,给instance加上volatile关键字。我们上一篇多线程的文章介绍过这个关键字,volatile 修饰的变量, 能够保证 “内存可见性”。因此,修改的代码如下:

//单例模式-懒汉模式
class Singleton2{
    //1.不需要立即初始化
    public static volatile Singleton2 instance = null;
    //2.把构造方法设为private
    private Singleton2(){}
    //3.提供一个方法来获取以上实例,
    //只有真正需要用到这个实例的时候,才去真正创建这个实例
    public static Singleton2 getInstance(){
//如果这个条件成立,说明当前的单例是未被初始化的,存在线程安全,就需要进行加锁操作
        if(instance == null){
            synchronized (Singleton2.class){
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }

        return instance;
    }
}

【JavaEE】多线程案例——单例模式与阻塞队列_第8张图片


二、阻塞队列

  阻塞队列是什么?
  阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则。阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

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

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

1.生产者消费者模型

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

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

  这个模型其实很好理解,就比如我们平时买东西去超市买,但是超市里的东西并不是超市生产的,它在是工厂里面生产出来的。那么在这里,其实这个超市就是一个容器,而我们就是消费者,工厂就是生产者。

  生产者消费者模型,是在实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发的场景中。

  阻塞队列的两个有点,解耦与削峰填谷

(1)让多个服务器程序之间更充分的解耦合。

【JavaEE】多线程案例——单例模式与阻塞队列_第9张图片

(2)对于请求,能够进行“削峰填谷”,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.

  什么是削峰填谷呢?在我们的生活中有一些防洪大坝,比如说是三峡大坝。我们直到,在雨季的时候,水流量就会很大,那么我们的三峡大坝关闸蓄水,这样就可以保护下流水流量不会变得太大,不至于出现洪灾。待到了旱季,水流量很小的时候,三峡大坝就可以开闸放水,给下流提供充足的水源,不至于出现旱灾。


 回到服务器这里:

【JavaEE】多线程案例——单例模式与阻塞队列_第10张图片
  实际开发中使用到的“阻塞队列”并不是一个简单的数据结构,而是一个/一组专门的服务器程序,并且它提供的功能也不仅仅是阻塞队列的功能,还会在这基础之上提供更多的功能(对于数据持久化存储,支持多个数据通道,支持多节点容灾冗余备份,支持管理面板,方便配置参数…)

  这样的队列又起了个新的名字——消息队列。


2.标准库中的阻塞队列

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

  • BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
  • put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
  • BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();

3.生产者消费者模型

public class Demo22 {
    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();
    }
}

4.阻塞队列实现

  • 通过 “循环队列” 的方式来实现.
  • 使用 synchronized 进行加锁控制.
  • put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
  • take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait
class MyBlockingQueue {
    // 保存数据的本体
    private int[] data = new int[1000];
    // 有效元素个数
    private int size = 0;
    // 队首下标
    private int head = 0;
    // 队尾下标
    private int tail = 0;
    // 专门的锁对象
    private Object locker = new Object();

    // 入队列
    public void put(int value) throws InterruptedException {
        synchronized (locker) {
            if (size == data.length) {
                // 队列满了. 暂时先直接返回.
                // return;
                locker.wait();
            }
            // 把新的元素放到 tail 位置上.
            data[tail] = value;
            tail++;
            // 处理 tail 到达数组末尾的情况
            if (tail >= data.length) {
                tail = 0;
            }
            // tail = tail % data.length;
            size++;  // 千万别忘了. 插入完成之后要修改元素个数
            // 如果入队列成功, 则队列非空, 于是就唤醒 take 中的阻塞等待.
            locker.notify();
        }
    }

    // 出队列
    public Integer take() throws InterruptedException {
        synchronized (locker) {
            if (size == 0) {
                // 如果队列为空, 就返回一个非法值.
                // return null;
                locker.wait();
            }
            // 取出 head 位置的元素
            int ret = data[head];
            head++;
            if (head >= data.length) {
                head = 0;
            }
            size--;
            // take 成功之后, 就唤醒 put 中的等待.
            locker.notify();
            return ret;
        }
    }
}

public class Demo21 {
    private static MyBlockingQueue queue = new MyBlockingQueue();

    public static void main(String[] args) {
        // 实现一个简单的生产者消费者模型
        Thread producer = new Thread(() -> {
            int num = 0;
            while (true) {
                try {
                    System.out.println("生产了: " + num);
                    queue.put(num);
                    num++;
                    // 当生产者生产的慢一些的时候, 消费者就得跟着生产者的步伐走.
                    // Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    int num = queue.take();
                    System.out.println("消费了: " + num);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();
        
    }
}


最后的话

  此文写于一个纷乱的夜晚,四周的杂声此起彼伏,双眼睡意不断来袭,任务很重,停下不了,一停下,就陷入了被动,只能忍忍这些杂声。此外疫情什么时候到头啊,真希望天下太平啊。

你可能感兴趣的:(JavaEE,多线程,java-ee,java,后端)