用synchronized实现线程安全环形缓冲区的核心是用同一把锁保护读写指针和数组访问,确保状态变更原子性;通过取模实现逻辑循环,预留空位区分满/空,并用wait/notifyAll支持阻塞读写。

用 synchronized 实现线程安全的环形缓冲区,核心是**用锁保护读写指针和数组访问**,避免多个线程同时修改状态导致数据错乱或越界。它不是最极致的高性能方案,但清晰、可靠、适合中低并发场景,也是理解环形缓冲原理和并发控制的扎实起点。
基础结构设计:数组 + 两个指针 + 容量
环形缓冲区本质是一个固定大小的数组,配合读(head 或 readIndex)、写(tail 或 writeIndex)两个移动指针。关键不在于“首尾相连”的物理结构,而在于用取模(%)实现逻辑循环:
- 数组容量为
capacity,索引范围是0到capacity - 1 - 每次写入后,写指针更新为
(writeIndex + 1) % capacity - 每次读取后,读指针更新为
(readIndex + 1) % capacity - 缓冲区为空:
readIndex == writeIndex - 缓冲区已满:
(writeIndex + 1) % capacity == readIndex(预留一个空位区分满/空)
用 synchronized 保护所有共享状态
所有可能被多线程并发访问的字段——数组、读指针、写指针、以及判断满/空的逻辑——必须包裹在同一个锁下。最简单直接的方式是把整个读写方法声明为 synchronized:
-
public synchronized boolean offer(E item):非阻塞写入,成功返回true,满则返回false -
public synchronized E poll():非阻塞读取,有数据返回元素,空则返回null - 也可以提供
put()和take()方法,内部调用wait()和notifyAll()实现阻塞等待
注意:不能只给部分操作加锁,也不能用不同对象做锁(比如分别锁 this 和锁某个字段),否则无法保证读写之间的原子性。
立即学习“Java免费学习笔记(深入)”;
完整示例:带阻塞能力的 RingBuffer
下面是一个可运行的简化版,支持阻塞写入(put)和阻塞读取(take),使用 synchronized + wait/notifyAll 协作:
public class SynchronizedRingBuffer<E> {
private final Object[] buffer;
private int readIndex = 0;
private int writeIndex = 0;
private final int capacity;
<pre class="brush:php;toolbar:false;">public SynchronizedRingBuffer(int capacity) {
this.capacity = capacity;
this.buffer = new Object[capacity];
}
public synchronized void put(E item) throws InterruptedException {
while (isFull()) {
wait(); // 等待有空间
}
buffer[writeIndex] = item;
writeIndex = (writeIndex + 1) % capacity;
notifyAll(); // 唤醒可能等待读取的线程
}
public synchronized E take() throws InterruptedException {
while (isEmpty()) {
wait(); // 等待有数据
}
@SuppressWarnings("unchecked")
E item = (E) buffer[readIndex];
buffer[readIndex] = null; // 防止内存泄漏(对引用类型重要)
readIndex = (readIndex + 1) % capacity;
notifyAll(); // 唤醒可能等待写入的线程
return item;
}
private boolean isFull() {
return (writeIndex + 1) % capacity == readIndex;
}
private boolean isEmpty() {
return readIndex == writeIndex;
}}
这个实现满足基本线程安全要求:一次只有一个线程能进入 put 或 take,所有状态变更都在临界区内完成,wait/notifyAll 确保了生产者与消费者间的正确协作。
注意事项和常见陷阱
即使用了 synchronized,仍需留意几个细节:
-
空值清理:读取后将数组对应位置设为
null,防止对象长期被引用导致 GC 无法回收(尤其在存储大对象时) -
泛型擦除处理:Java 泛型在运行时不存在,所以底层用
Object[],取值时需强制转换并加@SuppressWarnings("unchecked") - 不要在同步块内做耗时操作:比如网络请求、文件读写,否则会严重拖慢其他线程;环形缓冲区本身只应做快速内存拷贝
-
异常安全:如果写入过程中抛出异常(如
NullPointerException),确保指针不会处于中间状态;本例中因操作极简,风险较低
它不复杂,但每一步都得落在实处。


















