11.从单例模式到阻塞队列:Java 并发必考知识点,大厂面试这样问
目录
(2)代码示例:用ArrayBlockingQueue实现生产者-消费者
作为一名大二下的学生,距离大三上冲刺大厂实习越来越近了。这段时间的学习和准备至关重要,今天结合自己的学习笔记,从面试建议、单例模式、阻塞队列三个核心点展开分享,希望能给同样在冲刺的同学们一些参考。
一、面试建议(心态与准备)
1. 关于面试的“N次面试,大概率会动的N=5左右”
面试中,很多时候会遇到“大概率会动”的情况(比如N≈5),这意味着没有核心亮点或准备不足时,很容易被fail。我们需要从技术、学校学历、沟通表达等方面提前准备,但更重要的是心态——情绪稳定、自信自信、找一面镜子多练,这会是影响你找工作心态的关键因素。
2. 实习与校招的时间线
-
6月过后,offer逐渐出来了;
-
有一批实习要结束了,9月Offer更多过来;
-
实习转正成功,可以提,也可以再找一波(只报大厂)。
我的思考:大二下到大三上是实习准备的黄金期,一定要抓住6-9月的时间窗口。未来即使实习转正,也可以尝试再冲一波大厂,增加自己的竞争力。
二、单例模式(Singleton Pattern)
单例模式的核心是:某个类在一个Java进程中,只能有一个实例,强制/编码阶段实现,饿汉式/类加载时创建实例,懒汉式/首次使用时创建实例。
1. 线程安全的单例模式(懒汉式优化)
class Singleton {
private volatile static Singleton instance = null; // volatile 保证可见性
// 私有构造方法,防止外部实例化
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查(双重检查)
instance = new Singleton();
}
}
}
return instance;
}
}
关键点:
-
volatile:防止指令重排序,保证多线程下的可见性; -
双重检查:减少锁的竞争,提高性能;
-
类加载时不会初始化,首次使用时才创建实例(懒汉式)。
2. JDBC设计成单例模式
JDBC的DataSource通常会设计成单例模式,因为数据库连接池只需要一个实例。示例代码如下:
package thread;
import com.mysql.cj.jdbc.MysqlDataSource;
import javax.sql.DataSource;
class Util {
private static volatile DataSource dataSource = null;
public static DataSource getDataSource() {
if (dataSource == null) {
synchronized (Util.class) {
if (dataSource == null) {
MysqlDataSource myDataSource = new MysqlDataSource();
myDataSource.setURL("jdbc:mysql://127.0.0.1:3306/java117?characterEncoding=utf8&useSSL=false");
myDataSource.setUser("root");
myDataSource.setPassword("root");
dataSource = myDataSource;
}
}
}
return dataSource;
}
}
思考:为什么JDBC用单例?因为数据库连接池是重量级资源,重复创建会消耗大量资源,单例能保证全局唯一,提高性能。
三、初识阻塞队列(BlockingQueue)
1. 阻塞队列的背景
-
常用的线程安全集合:
Vector(线程安全)、ArrayList(线程不安全)、StringBuffer(线程安全)、StringBuilder(线程不安全); -
业界更流行的是阻塞队列(BlockingQueue),它是“非常非常重要的数据结构”。
2. 阻塞队列的核心特性
-
先进先出(FIFO);
-
线程安全;
-
阻塞功能:
-
队列为空时,消费方阻塞,直到队列有数据;
-
队列满时,生产方阻塞,直到队列有空位。
-
3. 生产者-消费者模型(用饺子比喻)
-
生产者:我擀饺子皮(生产数据);
-
消费者:师娘和我妈包饺子(消费数据);
-
交易场所:盖帘(阻塞队列)。
举个例子:
-
方案一:三个人每人分一个饺子皮和一个饺子馅,自己擀自己包(无阻塞,效率低);
-
方案二:我擀饺子皮,师娘和我妈负责包(用盖帘存饺子皮,阻塞队列实现)。
我的思考:阻塞队列是解耦生产者和消费者的关键,让两者速度不匹配时,能通过“阻塞”或“唤醒”来平衡,避免资源浪费或系统崩溃。
4. 阻塞队列的作用
-
降低资源的竞争:生产者消费者不直接耦合,通过队列缓冲;
-
解耦合:服务A和服务B通过MQ(消息队列,本质是阻塞队列)通信,A的代码改了不影响B,B的bug也不影响A;
-
削峰填谷:比如选课系统,开学时请求量暴增,阻塞队列可以缓存请求,避免系统被冲垮。
5. Java中的BlockingQueue
Java标准库提供了BlockingQueue接口,常用实现类有ArrayBlockingQueue(数组实现,有界)、LinkedBlockingQueue(链表实现,可选有界)、SynchronousQueue(同步队列,无容量)等。
(1)BlockingQueue的基本操作
-
put(E e):如果队列满,阻塞直到有空位; -
take():如果队列空,阻塞直到有元素; -
offer(E e):非阻塞,队列满则返回false; -
poll():非阻塞,队列空则返回null。
(2)代码示例:用ArrayBlockingQueue实现生产者-消费者
package thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Demo7 {
public static void main(String[] args) {
// 创建一个容量为100的ArrayBlockingQueue
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(100);
// 生产者线程
Thread t1 = new Thread(() -> {
while (true) {
try {
int n = (int) (Math.random() * 100);
blockingQueue.put(n); // 阻塞式放入
System.out.println("生产:" + n);
Thread.sleep(100); // 模拟生产耗时
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 消费者线程
Thread t2 = new Thread(() -> {
while (true) {
try {
int n = blockingQueue.take(); // 阻塞式取出
System.out.println("消费:" + n);
Thread.sleep(500); // 模拟消费耗时
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
6. 自己实现简单的阻塞队列(基于数组)
为了理解阻塞队列的原理,我们可以尝试自己实现一个基于数组的循环队列,并支持阻塞功能。
(1)基本结构
class MyBlockingQueue {
private String[] data; // 存储数据的数组
private int head = 0; // 队头指针
private int tail = 0; // 队尾指针
private int size = 0; // 当前元素个数
private Object locker = new Object(); // 锁对象
public MyBlockingQueue(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity must be positive");
}
data = new String[capacity];
}
}
(2)put方法(生产)
public void put(String elem) {
synchronized (locker) {
if (size == data.length) { // 队列满,阻塞(这里先简单return,后续优化)
return;
}
data[tail] = elem;
tail++;
if (tail >= data.length) {
tail = 0; // 循环数组
}
size++;
locker.notify(); // 唤醒等待的消费者(如果有)
}
}
(3)take方法(消费)
public String take() {
synchronized (locker) {
if (size == 0) { // 队列空,阻塞(这里先简单return null,后续优化)
return null;
}
String ret = data[head];
head++;
if (head >= data.length) {
head = 0; // 循环数组
}
size--;
locker.notify(); // 唤醒等待的生产者(如果有)
return ret;
}
}
(4)阻塞功能的优化(wait/notify)
上面的put和take只是简单的“满则return、空则null”,真正的阻塞需要用wait()和notify():
public void put(String elem) {
synchronized (locker) {
while (size == data.length) { // 用while防止虚假唤醒
try {
locker.wait(); // 队列满,等待
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
data[tail] = elem;
tail++;
if (tail >= data.length) {
tail = 0;
}
size++;
locker.notify(); // 唤醒消费者
}
}
public String take() {
synchronized (locker) {
while (size == 0) { // 用while防止虚假唤醒
try {
locker.wait(); // 队列空,等待
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
String ret = data[head];
head++;
if (head >= data.length) {
head = 0;
}
size--;
locker.notify(); // 唤醒生产者
return ret;
}
}
思考:自己实现阻塞队列时,wait()和notify()的使用是关键。while循环防止“虚假唤醒”(JVM层面的线程唤醒),notify()唤醒等待的线程,保证生产消费的平衡。
四、学习总结与实习冲刺建议
1. 技术深度与广度
-
单例模式不仅要会写,还要理解线程安全、性能优化(双重检查、volatile);
-
阻塞队列要理解生产者-消费者模型、解耦、削峰填谷的实际意义,能手写简单的阻塞队列;
-
多线程相关的集合(Vector、ArrayList、BlockingQueue等)要区分线程安全和阻塞特性。
2. 实习准备节奏
-
大二下:夯实基础(多线程、设计模式、数据结构),刷LeetCode(重点中等题),做小项目(比如用阻塞队列实现生产者消费者);
-
大三上:投递实习,面试前多模拟(找同学互面、对着镜子练),心态放平(面试是双向选择)。
3. 心态调整
-
面试有失败很正常,每一次失败都是经验;
-
情绪稳定、自信表达,比“完美答案”更重要;
-
把面试当成“技术交流”,而非“考试”,能更放松地展现自己。
最后的话
大二下到大三上的这段时光,是冲大厂实习的关键期。希望这篇博客能帮你梳理面试建议、单例模式、阻塞队列的核心知识点,也能给你一些心态和方法上的参考。加油,我们一起冲向大厂
更多推荐

所有评论(0)