当前位置: 首页 > java >正文

Java EE初阶——单列模式和阻塞队列

1. 单列模式

单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点。它常用于管理共享资源(如数据库连接、线程池、配置信息等)。单例模式在多线程环境下需要特别注意线程安全问题。

1. 单例模式的特点

  • 私有化构造函数(防止外部 new 创建实例)

  • 静态私有实例变量(存储唯一实例)

  • 静态公有方法(提供全局访问点)

1. 饿汉模式

class EagerSingleton{// 类加载时就创建实例private static EagerSingleton instance = new EagerSingleton();// 提供全局访问点public static EagerSingleton getInstance(){return instance;}// 私有构造函数,防止外部实例化private EagerSingleton(){};
}

其他代码如果想要使用这个类的实例,不能使用 new 创建对象,而是调用 getInstance() 这个方法来获取已经创建好的对象。

优点:线程安全,在类加载时就创建实例,保证在多线程环境下也能正确工作。
缺点:即使实例未被使用,也会在类加载时创建,可能造成资源浪费。

2. 懒汉模式

1. 线程不安全

class LazySingleton{// 声明静态实例变量,但不立即初始化private static LazySingleton instance = null;//提供全局访问点public static LazySingleton getInstance(){// 第一次调用时检查实例是否存在if(instance == null){//若不存在,创建新实例(仅执行一次)instance = new LazySingleton();}// 返回实例(已存在或新创建)return instance;}// 私有构造函数,防止外部通过new创建实例private LazySingleton(){};
}

优点:只有在第一次调用 getInstance() 方法时才创建实例,避免资源浪费。
缺点

  1. 线程不安全:在多线程环境下,可能会出现多个线程同时判断 instance 为 null,从而创建多个实例,不具备线程安全性
  2. 内存浪费:如果这个实例化对象非常大,创建多个会占用不必要的内存。
  3. 状态不一致:不同线程获取到不同的实例,破坏单例模式的设计初衷。

2. 线程安全,同步方法

class LazySingleton2{private static LazySingleton2 instance = null;public static Object locker = new Object();public static LazySingleton2 getInstance(){//加锁synchronized (locker){if(instance == null){instance = new LazySingleton2();}}return instance;}private LazySingleton2(){};
}
public class Singleton3 {
}

优点:确保在多线程环境下只有一个实例,线程安全。
缺点:每次调用 getInstance() 方法都需要获取锁,性能较低,尤其是在高并发场景下。

3. 双重检查锁定(DCL)单例

class LazySingleton3{private static LazySingleton3 instance = null;private static Object locker = new Object();//锁对象public static LazySingleton3 getInstance(){//判断是否需要加锁if(instance == null){//加锁synchronized (locker){//判断是否需要创建对象if(instance == null){instance = new LazySingleton3();}}}return instance;}private LazySingleton3(){};
}

优点:在保证线程安全的同时,只有在第一次创建实例时才需要同步,后续调用直接返回实例,避免了每次调用都加锁提高了性能。
缺点

instance = new LazySingleton3(); 可以拆成三个步骤:

   1. 申请一段内存空间

   2. 在这个内存上调用构造方法,创建出这个实例

   3. 把这个内存地址赋值给 instance 引用变量

在多线程环境中,指令重排序,可能造成 1,3,2 的执行顺序。实际修改了写 instance 变量的执行顺序。

4. 使用 volatile 关键字,防止指令重排序

private static volatile LazySingleton3 instance = null;

volatile 确保变量的可见性禁止指令重排序,保证线程安全。

2. 阻塞队列

阻塞队列是一种特殊的队列数据结构,也遵守 "先进先出" 的原则,其核心特点是:

  • 线程安全:内部通过锁机制保证多线程操作的安全性。
  • 阻塞操作
    • 当队列已满时,插入操作会被阻塞,直到队列有空闲空间。
    • 当队列为空时,取出操作会被阻塞,直到队列中有元素可用。

阻塞队列常用于多线程编程中的线程间通信(生产者 - 消费者模型),能有效简化同步逻辑,避免线程忙等待或竞态条件。

1. 生产者 - 消费者模型

1. 解耦合

核心思想是通过一个共享缓冲区(队列)来解耦生产者和消费者,使两者无需直接交互,从而提高系统的并发性和可维护性。

核心组成部分

  1. 生产者(Producer)

    • 负责生成数据,并将数据放入共享缓冲区。
    • 当缓冲区已满时,生产者需要等待(阻塞),直到缓冲区有空间。
  2. 消费者(Consumer)

    • 负责从共享缓冲区中取出数据并处理。
    • 当缓冲区为空时,消费者需要等待(阻塞),直到缓冲区有数据。
  3. 共享缓冲区(Blocking Queue)

    • 作为生产者和消费者之间的 “桥梁”,存储待处理的数据。
    • 具备阻塞特性

2. 削峰填谷

削峰填谷(Peak Shaving and Valley Filling)是计算机系统设计中常用的流量管理和性能优化策略,核心思想是通过缓冲、队列或缓存等机制,将瞬时高峰流量(峰值)平滑化,避免系统因突发流量过载而崩溃,同时在流量低谷时充分利用系统资源处理积压任务。

核心目标

  1. 保护系统稳定性
    防止瞬时流量峰值超过系统处理能力,导致服务降级、超时或宕机。
  2. 优化资源利用率
    在流量低谷时处理积压任务,避免资源(如 CPU、内存、I/O)因流量波动而闲置。
  3. 实现流量平滑化
    将突发流量转换为稳定的处理流,使系统负载均匀,提升整体吞吐量。

阻塞操作

    • 插入(put):当队列满时,生产者线程阻塞,直到队列有空余位置(削峰:防止瞬时大量请求冲击消费者)。
    • 取出(take):当队列空时,消费者线程阻塞,直到队列有数据(填谷:避免消费者空闲)。

    为什么需要生产者 - 消费者模型?

    1. 解耦生产者和消费者
      两者无需直接通信,只需关注各自的业务逻辑,降低代码耦合度。
    2. 支持异步处理
      生产者生成数据后无需等待消费者处理,可直接继续生产,提高系统吞吐量。
    3. 平衡生产 / 消费速度差异
      当生产者和消费者处理速度不匹配时,缓冲区可作为 “缓冲池” 暂存数据,避免数据丢失或系统过载。

    2. 阻塞队列实现(Java)

    Java 标准库的阻塞队列通过统一接口(BlockingQueue)提供了丰富的实现,满足不同场景下的线程安全队列需求。

    阻塞队列核心方法对比

    操作类型抛出异常(如失败)返回特殊值(如失败)阻塞等待(直到成功)超时等待(指定时间内)
    插入元素add(e)offer(e)put(e)offer(e, time, unit)
    取出元素remove()poll()take()poll(time, unit)
    查看头部元素element()peek()--

    Java 并发包(java.util.concurrent)中提供的阻塞队列实现类,适用于不同场景:

    1. ArrayBlockingQueue

    • 特性
      • 有界队列:初始化时需指定固定容量,无法动态扩容。
      • 基于数组实现:内部使用数组存储元素,性能较高。
      • 公平性可选:可通过构造函数指定是否按线程等待顺序获取锁(公平锁性能略低)。
    // 创建容量为100的队列,默认非公平锁
    BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100);// 创建公平锁队列(先等待的线程优先获取锁)
    BlockingQueue<Integer> fairQueue = new ArrayBlockingQueue<>(100, true);
    • 适用场景
      • 需要严格控制队列容量上限的场景(如内存有限的系统)。
      • 生产者和消费者速度差异不大的场景。

    2. LinkedBlockingQueue

    • 特性
      • 可选有界队列:若不指定容量,默认使用 Integer.MAX_VALUE(视为无界队列)。
      • 基于链表实现:插入和删除操作性能较高(无需扩容)。
      • 双锁机制:使用两把锁分别控制队头和队尾的访问,提升并发性能。
    // 创建无界队列(实际容量受内存限制)
    BlockingQueue<String> unboundedQueue = new LinkedBlockingQueue<>();// 创建有界队列(容量为500)
    BlockingQueue<String> boundedQueue = new LinkedBlockingQueue<>(500);
    • 适用场景
      • 生产者速度远大于消费者速度,需缓冲大量数据的场景(如日志收集)。
      • 需高并发读写的场景(双锁机制优于 ArrayBlockingQueue)。

    3. PriorityBlockingQueue

    • 特性
      • 无界优先队列:元素按优先级排序(需实现 Comparable 接口或自定义 Comparator)。
      • 基于堆结构:插入和删除元素的时间复杂度为 O (log n)。
      • 不保证同优先级元素的顺序:仅保证取出时优先级最高的元素优先。
    class Task implements Comparable<Task> {private int priority;public Task(int priority) { this.priority = priority; }@Overridepublic int compareTo(Task o) {return Integer.compare(o.priority, this.priority); // 优先级高的先出队}
    }// 创建优先队列
    BlockingQueue<Task> queue = new PriorityBlockingQueue<>();
    queue.put(new Task(3)); // 低优先级
    queue.put(new Task(1)); // 高优先级
    Task task = queue.take(); // 取出优先级最高的任务(1)

    3. ArrayBlockingQueue 实现

    1. 先实现普通队列

    2. 再加上线程安全

    3. 再加上阻塞队列

    class MyArrayBlockingQueue {public int head = 0;//头节点public int tail = 0;//尾节点public int size = 0;//记录当前队列中元素个数private String[] elem = null;//存储元素的环形数组public MyArrayBlockingQueue(int capacity){elem = new String[capacity];}private Object object = new Object();//锁对象,用于同步// 生产者方法:向队列添加元素(队列满时阻塞)public void put(String s) throws InterruptedException {synchronized (object){// 用while而非if:防止虚假唤醒或唤醒后条件已变化while(size >= elem.length){//队列满,阻塞object.wait();}// 2. 插入元素到队尾elem[tail] = s;tail++;if(tail>=elem.length){tail=0;// 环形数组:指针回到头部}size++;// 3. 唤醒可能等待的消费者线程object.notify();}}
    // 消费者方法:从队列取出元素(队列空时阻塞)public String take() throws InterruptedException {String s = null;synchronized (object){// 用while而非if:防止虚假唤醒或唤醒后条件已变化while(size==0){//队列空,阻塞object.wait();}// 2. 从队头取出元素s = elem[head];head++;if(head>=elem.length){head = 0; // 环形数组:指针回到头部}size--;// 3. 唤醒可能等待的生产者线程object.notify();}return s;}
    }
    public class Test {public static void main(String[] args) throws InterruptedException {MyArrayBlockingQueue queue = new MyArrayBlockingQueue(1000);queue.put("aaa");queue.put("bbb");queue.put("ccc");System.out.println(queue.take());System.out.println(queue.take());System.out.println(queue.take());}
    }

    1. 锁的位置不同:

    2. 使用if:

    synchronized 和 while 通常结合使用,用于实现线程间的等待 - 通知机制(Wait-Notify)。这种组合能有效避免线程间的竞态条件和虚假唤醒(Spurious Wakeup),确保线程安全。

    1. 为什么需要 while 而非 if
    • 虚假唤醒:线程可能在未收到明确通知的情况下被唤醒(如操作系统或 JVM 的内部优化)。
    • 条件变化:当多个线程共享同一条件时,一个线程唤醒后,条件可能已被其他线程修改。
    2. while 循环的必要性
    • 防止虚假唤醒:即使线程被意外唤醒,while 会再次检查条件,若不满足则继续等待。
    • 处理多线程竞争:若多个消费者被唤醒,其中一个取走元素后,其他消费者会发现队列又空了,继续等待。
    http://www.xdnf.cn/news/7393.html

    相关文章:

  • 深入解析RAG技术:提升题目解答准确率的利器
  • turf的pointsWithinPolygon排查
  • window xampp apache使用腾讯云ssl证书配置https
  • 算法(最小基因变化+迷宫中离入口最近的出口)
  • C# 枚举 详解
  • linux kernel 编译
  • java的arraylist集合
  • TransactionSynchronizationManager事务同步器的使用
  • 统计客户端使用情况,使用es存储数据,实现去重以及计数
  • 【全解析】EN18031标准下的SCM安全通信机制全解析
  • 质检LIMS系统检测数据可视化大屏 全流程提效 + 合规安全双保障方案
  • 视频监控中的存储方式有哪些?EasyCVR视频监控汇聚平台如何打造高效监控存储
  • 高速系统设计实例设计分析之三
  • 蓝桥杯2300 质数拆分
  • 码蹄集——N是什么、棋盘
  • JVM(Java 虚拟机)深度解析
  • web基础常用标签
  • More Effective C++:改善编程与设计(下)
  • Seata源码—6.Seata AT模式的数据源代理三
  • 洛谷U536262 井底之“鸡” 附视频讲解
  • 提示词专家的修炼秘籍
  • harris角点检测
  • VisionPro:轴承错位标识
  • QT之绘图模块和双缓冲技术
  • MapStruct Date 转 LocalDate 偏差一天问题
  • 【C++】异常解析
  • AGI大模型(28):LangChain提示模板
  • MySQL中的Change Buffer是什么,它有什么作用?
  • 火山 RTC 引擎9 ----集成 appkey
  • 5月19日笔记