首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Javaee ——案例 单例模式和阻塞队列

Javaee ——案例 单例模式和阻塞队列

作者头像
Han.miracle
发布2025-12-23 09:31:33
发布2025-12-23 09:31:33
150
举报

一、单例模式

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

啥是设计模式?

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

软件开发中也有很多常见的"问题场景",针对这些问题场景,大佬们总结出了⼀些固定的套路,按照这个套路来实现代码,也不会吃亏 单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例

这一点在很多场景上都需要,比如JDBC中的DataSource实例就只需要一个

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

1、饿汉模式

类加载的同时创建实例

代码语言:javascript
复制
 class Singleton {
     private static Singleton instance = new Singleton();
     private Singleton() {}
     public static Singleton getInstance() {
         return instance;
     }
 }
1.1懒汉模式---单线程版

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

代码语言:javascript
复制
 class Singleton {
     private static Singleton instance = null;
     private Singleton() {}
     public static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
     }
 }

懒汉的问题:

左边是t1 右边是t2

这说好的创建一次实例呢,这不就是创建两次了么(这虽然被覆盖了,而且随着第二次的操作覆盖了,第一个线程new 的对象,也会被GC给释放掉)

1.2懒汉模式---多线程版

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

线程安全问题发生在首次创建实例时,如果在多个线程中同时调用 getInstance方法,就可能导致创建出多个实例。 一旦实例已经创建好了,后面再多线程环境调用getInstance就不再有线程安全问题了(不再修改 instance 了)

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

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

当然把他synchronized 修饰方法也可以实现

一、代码逻辑与线程安全保障

这段代码是懒汉式单例的 getInstance 方法,通过 synchronized (Locker) 加锁,确保多线程下只有一个线程能创建实例(当 instance == null 时,执行 instance = new SingletonLazy()),从而保证单例的唯一性。

二、性能问题的核心原因

当实例创建完成后,后续所有调用 getInstance 的线程:

  • 仅需执行 “if (instance == null) 判断 + return instance”(纯粹的读操作,无线程安全风险)。
  • 但每次调用仍会触发加锁操作,多线程环境下会因锁竞争相互阻塞,导致执行效率降低。
三、问题本质

这种实现 “过度加锁”—— 在不需要线程安全保护的读操作阶段,仍强制进行锁竞争,从而产生不必要的性能开销。

(可进一步优化为 “双重检查锁” 模式,即先判断 instance == null 再加锁,避免后续读操作的无效加锁,平衡线程安全与执行效率。)

但是效率就慢了

但是还有一种隐藏的问题就是:

他这里有可能是 进行编译器优化,这个是广义的编译器,javac ,jvm 甚至是操作系统都要配合的

进行编译器优化,这里的指令运行顺序就会被打乱,但是逻辑是一样的,但是这个指令被打乱,出现误判,就会出现多线程的问题

.3懒汉模式---多线程版(改进)

以下代码在加锁的基础上,做出了进一步改动:

使用双重if判定,降低锁竞争的频率

给instance加上了volatile

代码语言:javascript
复制
 class Singleton {
     private static volatile Singleton instance = null;
     private Singleton() {}
     public static Singleton getInstance() {
         if (instance == null) {
             synchronized (Singleton.class) {
                 if (instance == null) {
                     instance = new Singleton();
                 }
             }
         }
         return instance;
     }
 }

理解双重if判定/volatile: 加锁/解锁是⼀件开销比较⾼的事情,而懒汉模式的线程不安全只是发生在首次创建实例的时候,因此后续使⽤的时候,不必再进行加锁了。 外层的if就是判定下看当前是否已经把instance实例创建出来了。 同时为了避免"内存可见性"导致读取的instance出现偏差,于是补充上volatile。 当多线程首次调用getInstance,大家可能都发现instance为null,于是又继续往下执行来竞争锁,其中竞争成功的线程,再完成创建实例的操作。 当这个实例创建完了之后,其他竞争到锁的线程就被里层if挡住了,也就不会继续创建其他实例

一、问题根源:对象创建的指令重排序

创建对象(instance = new SingletonLazy())在 JVM 中会被拆分为 3 步核心指令:

  1. 分配内存空间(给新对象开辟内存);
  2. 初始化对象(调用构造方法,初始化成员变量);
  3. 将引用指向内存地址instance 指向刚分配的内存,此时 instance != null)。

JVM 为了优化性能,可能会对这 3 步指令重排序(不影响单线程执行结果),比如重排为:1 → 3 → 2(先分配内存、让引用指向地址,再初始化对象)。

二、重排序导致的问题
代码语言:javascript
复制
private static volatile SingletonLazy instance; // 关键:volatile修饰

假设线程 A 执行到 “创建实例” 时,指令被重排为 1→3→2

  • 线程 A 执行完步骤 3(instance 已非空),但步骤 2(对象初始化)尚未完成;
  • 此时线程 B 进入 getInstance 方法,第一次检查 instance != null,直接返回这个未初始化完成的对象
  • 线程 B 使用该对象时,可能因对象未初始化而触发异常(如空指针)。
三、解决方案:用 volatile 禁止重排序

在单例变量 instance 前添加 volatile 关键字,可解决此问题:

1. 有三个线程,开始执行getInstance ,,通过外层的 if (instance == null) 知道了实例还没有创建的消息,于是开始竞争同⼀把锁

2. 其中线程1率先获取到锁,此时线程1通过里层的 if (instance == null) 进一步确认实例是否已经创建,如果没创建,就把这个实例创建出来。

3. 当线程1释放锁之后,线程2和线程3也拿到锁,也通过里层的 if (instance == null) 来确认实例是否已经创建,发现实例已经创建出来了,就不再创建了。

4. 后续的线程,不必加锁,直接就通过外层 if (instance == null) 就知道实例已经创建了, 从⽽不再尝试获取锁了.降低了开销

二、阻塞队列

1、阻塞队列是什么 阻塞队列是一种特殊的队列,也遵守"先进先出"的原则。

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

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

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

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

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

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

1)阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力(削峰填⾕)

比如在"秒杀"场景下,服务器同一时刻可能会收到大量的支付请求,如果直接处理这些⽀付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程),这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个支付请求。

这样做可以有效进行"削峰",防止服务器被突然到来的一波请求直接冲垮。

2)阻塞队列也能使生产者和消费者之间解耦

比如过年一家人一起包饺子,一般都是有明确分工,比如⼀个人负责擀饺子皮,其他人负责包,擀饺子皮的人就是"生产者",包饺子的人就是"消费者"。

擀饺子皮的人不关心包饺子的人是谁(能包就行,无论是手工包,借助工具,还是机器包),包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行,无论是用擀面杖擀的,还是拿罐头瓶擀,还是直接从超市买的)

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

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

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

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

代码语言:javascript
复制
 BlockingQueue<String> queue = new LinkedBlockingQueue<>();
 // ⼊队列 
 queue.put("abc");
 // 出队列. 如果没有 put 直接 take, 就会阻塞.  
 String elem = queue.take();

执行到 队列已经满了,在queue.put()在这就已经堵塞了,后面就不会执行了

生产者消费者模型

代码语言:javascript
复制
 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) 循环的目的:是为了反复验证

判定这里面的逻辑是否还是正确的

二次验证,这里的wait 是否还是 size = 0

wait 有可能被NotifyAll()的 唤醒 或者 被Interrupter

代码语言:javascript
复制
public class BlockingQueue {
    private int[] items = new int[1000];
    private volatile int size = 0;
    private volatile int head = 0;
    private volatile int tail = 0;
    public void put(int value) throws InterruptedException {
        synchronized (this) {
            // 此处最好使⽤ while. 
            // 否则 notifyAll 的时候, 该线程从 wait 中被唤醒, 
            // 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能⼜已经队列满了 
            // 就只能继续等待 
            while (size == items.length) {
                wait();
            }
            items[tail] = value;
            tail = (tail + 1) % items.length;
            size++;
           notifyAll();
        }
    }
    public int take() throws InterruptedException {
        int ret = 0;
        synchronized (this) {
            while (size == 0) {
                wait();
            }
            ret = items[head];
            head = (head + 1) % items.length;
            size--;
            notifyAll();
        }
        return ret;
    }
    public synchronized int size() {
        return size;
    }
 
    // 测试代码 
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue blockingQueue = new BlockingQueue();
        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 {
                    blockingQueue.put(random.nextInt(10000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
         }, "⽣产者");
         producer.start();
         customer.join();
         producer.join();
     }
 }

消息队列的一点点扩展

代码语言:javascript
复制
package thread;

import java.util.Arrays;

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++;
                    } 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();
        }
    }

    class MyBlockingQueue {
    //用数组实现这个queue
    public String[] array = null;
    public volatile int head = 0;
    public volatile int tail = 0;
    public volatile int size = 0;
    public MyBlockingQueue(int Capitial){
       array  = new String[Capitial];
    }


    public void put(String string) throws InterruptedException {
        synchronized (this) {
            while (size == array.length) {
                wait();
            }
            array[tail] = string;
            tail++;
            if (tail >= array.length) {
                tail = 0;
            }
            notify();
        }
    }

    public String  take() throws InterruptedException {
        synchronized (this) {
            while (size == array.length) {
                wait();
            }
            String ret = array[head];
            array[head] = null;
            head++;
            if(head >= array.length){
                head = 0;
            }
size--;
notify();
return ret;
        }
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、单例模式
    • 1、饿汉模式
    • 一、代码逻辑与线程安全保障
    • 二、性能问题的核心原因
    • 三、问题本质
    • 一、问题根源:对象创建的指令重排序
    • 二、重排序导致的问题
    • 三、解决方案:用 volatile 禁止重排序
  • 二、阻塞队列
    • 1、阻塞队列是什么 阻塞队列是一种特殊的队列,也遵守"先进先出"的原则。
    • 2、生产者消费者模型 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
    • 3、标准库中的阻塞队列 在Java标准库中内置了阻塞队列,如果我们需要在一些程序中使用阻塞队列,直接使用标准库中的即可
    • take取出元素的时候,判定如果队列为空,就进行wait。(也是循环wait) 循环的目的:是为了反复验证
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档