多线程是并发编程的核心概念,也是面试中的高频考点。本文将围绕多线程中的单例模式、阻塞队列及生产者消费者模型展开,结合代码实例帮助大家理解这些基础知识点。
一、单例模式:懒汉与饿汉
单例模式是最常用的设计模式之一,其核心是保证一个类在全局只有唯一实例,并提供一个访问该实例的全局入口。在多线程场景下,实现安全的单例模式需要特别注意线程同步问题。
1. 饿汉模式
饿汉模式的特点是"饿",即类加载时就创建实例,天然线程安全。
java
// 饿汉模式
public class SingletonHungry {
// 类加载时就初始化实例
private static final SingletonHungry instance = new SingletonHungry();
// 私有构造方法,防止外部实例化
private SingletonHungry() {}
// 提供全局访问点
public static SingletonHungry getInstance() {
return instance;
}
}
优点 :实现简单,线程安全(类加载过程由JVM保证线程安全)
缺点:如果实例创建成本高且长时间不使用,会造成资源浪费
2. 懒汉模式(线程安全版)
懒汉模式的特点是"懒",即用到时才创建实例。但需要通过同步锁和双重检确保线程安全。
java
// 懒汉模式(线程安全版)
public class SingletonLazy {
// volatile修饰:防止指令重排序导致的空指针问题
private static volatile SingletonLazy instance=null;
private static Object locker=new Object();
// 私有构造方法
private SingletonLazy() {}
// 双重检查锁定(Double-Checked Locking)
public static SingletonLazy getInstance() {
// 第一次检查:避免不必要的锁竞争
if (instance == null) {
// 加锁:保证同步创建实例
synchronized (locker) {
// 第二次检查:防止多线程同时通过第一次检查后重复创建
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
关键技术点解析:
- 双重if检查:外层if避免每次调用都加锁(提高效率),内层if防止多线程同时进入临界区后重复创建实例
- volatile修饰 :
instance = new SingletonLazy()实际分为3步(分配内存→初始化对象→赋值引用),volatile可禁止指令重排序,避免其他线程获取到未初始化的实例
二、阻塞队列:并发场景的"缓冲区"
阻塞队列(BlockingQueue)是多线程编程中的重要工具,它提供了一种线程安全的队列操作方式,核心特性是:
- 阻塞特性:当队列满时,入队操作阻塞;当队列空时,出队操作阻塞
- 削峰填谷:平衡生产者和消费者的处理能力(比如秒杀场景中缓冲突发请求)
- 解耦作用:生产者和消费者无需知道对方存在,通过队列间接交互
基于阻塞队列的生产者消费者模型
生产者消费者模型是并发编程中的经典模式,通过阻塞队列实现可大幅简化代码:
java
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class Demo27 {
public static void main(String[] args) {
BlockingDeque<Integer> deque=new LinkedBlockingDeque<>();
Thread producer=new Thread(()->{
int n=0;
while (true){
try {
Thread.sleep(500);
deque.put(n);
System.out.println("生产"+n);
n++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"producer");
Thread consumer=new Thread(()->{
while (true){
try {
Thread.sleep(1000);
Integer n = deque.take();
System.out.println("消费" + n);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"consumer");
producer.start();
consumer.start();
}
}
运行效果 :
生产者每0.5秒生产一个数据,消费者每1秒消费一个数据.
三、手动实现阻塞队列
理解阻塞队列的内部原理,有助于更好地使用它。我们可以基于数组和ReentrantLock实现一个简单的阻塞队列:
java
class MyBlockingQueue {
private String[] data = null;
private int head = 0;
private int tail = 0;
private int size = 0;
public MyBlockingQueue(int capacity) {
data = new String[capacity];
}
public void put(String elem) throws InterruptedException {
synchronized (this) {
while (size >= data.length) {
this.wait();
}
data[tail++] = elem;
if (tail >= data.length) {
tail = 0;
}
//tail=(tail+1)%data.length;}
size++;
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this) {
while (size == 0) {
this.wait();
}
String ret = data[head];
head++;
if (head >= data.length) {
head = 0;
}
size--;
this.notify();
return ret;
}
}
}
public class Demo28 {
public static void main(String[] args) {
MyBlockingQueue queue=new MyBlockingQueue(50);
Thread producer=new Thread(()->{
int n=0;
while (true){
try {
queue.put(n+"");
System.out.println("生产元素"+n);
n++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread consumer=new Thread(()->{
String n=null;
while (true){
try {
Thread.sleep(1000);
n=queue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("消费元素"+n);
}
});
producer.start();
consumer.start();
}
}
核心实现思路:
- 用数组实现循环队列(通过取模运算处理指针越界)
- 用ReentrantLock保证线程安全,避免并发问题
- 用两个Condition对象分别处理"队列满"和"队列空"的阻塞等待
总结
本文介绍了多线程中的三个核心知识点:
- 单例模式的两种实现(饿汉模式简单安全,懒汉模式需双重检查+volatile保证线程安全)
- 阻塞队列的特性及生产者消费者模型(解耦+削峰填谷的关键工具)
- 阻塞队列的手动实现(基于锁和条件变量的经典并发编程实践)
掌握这些基础知识,是深入学习多线程编程的基础。实际开发中,JDK提供了丰富的并发工具类(如ArrayBlockingQueue、ConcurrentHashMap等),理解其底层原理能帮助我们更好地运用它们解决实际问题。