认识多线程:单例模式
多线程是并发编程的核心概念,也是面试中的高频考点。本文将围绕多线程中的单例模式、阻塞队列及生产者消费者模型展开,结合代码实例帮助大家理解这些基础知识点。
一、单例模式:懒汉与饿汉
单例模式是最常用的设计模式之一,其核心是保证一个类在全局只有唯一实例,并提供一个访问该实例的全局入口。在多线程场景下,实现安全的单例模式需要特别注意线程同步问题。
1. 饿汉模式
饿汉模式的特点是"饿",即类加载时就创建实例,天然线程安全。
// 饿汉模式
public class SingletonHungry {// 类加载时就初始化实例private static final SingletonHungry instance = new SingletonHungry();// 私有构造方法,防止外部实例化private SingletonHungry() {}// 提供全局访问点public static SingletonHungry getInstance() {return instance;}
}
优点:实现简单,线程安全(类加载过程由JVM保证线程安全)
缺点:如果实例创建成本高且长时间不使用,会造成资源浪费
2. 懒汉模式(线程安全版)
懒汉模式的特点是"懒",即用到时才创建实例。但需要通过同步锁和双重检确保线程安全。
// 懒汉模式(线程安全版)
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)是多线程编程中的重要工具,它提供了一种线程安全的队列操作方式,核心特性是:
- 阻塞特性:当队列满时,入队操作阻塞;当队列空时,出队操作阻塞
- 削峰填谷:平衡生产者和消费者的处理能力(比如秒杀场景中缓冲突发请求)
- 解耦作用:生产者和消费者无需知道对方存在,通过队列间接交互
基于阻塞队列的生产者消费者模型
生产者消费者模型是并发编程中的经典模式,通过阻塞队列实现可大幅简化代码:
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实现一个简单的阻塞队列:
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等),理解其底层原理能帮助我们更好地运用它们解决实际问题。
