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()
方法时才创建实例,避免资源浪费。
缺点:
- 线程不安全:在多线程环境下,可能会出现多个线程同时判断
instance
为null
,从而创建多个实例,不具备线程安全性 - 内存浪费:如果这个实例化对象非常大,创建多个会占用不必要的内存。
- 状态不一致:不同线程获取到不同的实例,破坏单例模式的设计初衷。
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. 解耦合
核心思想是通过一个共享缓冲区(队列)来解耦生产者和消费者,使两者无需直接交互,从而提高系统的并发性和可维护性。
核心组成部分
-
生产者(Producer)
- 负责生成数据,并将数据放入共享缓冲区。
- 当缓冲区已满时,生产者需要等待(阻塞),直到缓冲区有空间。
-
消费者(Consumer)
- 负责从共享缓冲区中取出数据并处理。
- 当缓冲区为空时,消费者需要等待(阻塞),直到缓冲区有数据。
-
共享缓冲区(Blocking Queue)
- 作为生产者和消费者之间的 “桥梁”,存储待处理的数据。
- 具备阻塞特性
2. 削峰填谷
削峰填谷(Peak Shaving and Valley Filling)是计算机系统设计中常用的流量管理和性能优化策略,核心思想是通过缓冲、队列或缓存等机制,将瞬时高峰流量(峰值)平滑化,避免系统因突发流量过载而崩溃,同时在流量低谷时充分利用系统资源处理积压任务。
核心目标
- 保护系统稳定性
防止瞬时流量峰值超过系统处理能力,导致服务降级、超时或宕机。 - 优化资源利用率
在流量低谷时处理积压任务,避免资源(如 CPU、内存、I/O)因流量波动而闲置。 - 实现流量平滑化
将突发流量转换为稳定的处理流,使系统负载均匀,提升整体吞吐量。
阻塞操作:
- 插入(put):当队列满时,生产者线程阻塞,直到队列有空余位置(削峰:防止瞬时大量请求冲击消费者)。
- 取出(take):当队列空时,消费者线程阻塞,直到队列有数据(填谷:避免消费者空闲)。
为什么需要生产者 - 消费者模型?
- 解耦生产者和消费者
两者无需直接通信,只需关注各自的业务逻辑,降低代码耦合度。 - 支持异步处理
生产者生成数据后无需等待消费者处理,可直接继续生产,提高系统吞吐量。 - 平衡生产 / 消费速度差异
当生产者和消费者处理速度不匹配时,缓冲区可作为 “缓冲池” 暂存数据,避免数据丢失或系统过载。
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
会再次检查条件,若不满足则继续等待。 - 处理多线程竞争:若多个消费者被唤醒,其中一个取走元素后,其他消费者会发现队列又空了,继续等待。