本文目录导读:

- 核心设计原则(先于代码)
- 标准同步处理流程(以
synchronized为例) - 使用
ReentrantLock的规范流程(更灵活的场景) - 关于“流程”的进阶规范:等待-通知模式 (Wait/Notify)
- 总结:规范同步流程的“清单”
- 例外情况处理
在Java中,规范处理同步(Synchronization)是保证多线程安全、避免死锁和性能瓶颈的核心,这里的“同步处理流程”通常指使用锁(如 synchronized、ReentrantLock)或原子类管理共享资源访问的模式。
下面我将从设计原则、标准编码流程、常见陷阱及规范几个方面为你拆解。
核心设计原则(先于代码)
在写同步代码前,先思考以下3点:
- 最小化锁的范围:只锁必要的代码块,不要锁整个方法(尤其是耗时操作)。
- 明确锁的对象:锁的是
this、类对象Class、还是某个专门的final锁对象?确保多个线程看到的是同一把锁。 - 避免死锁:如果存在多个锁,保证所有线程获取锁的顺序一致。
标准同步处理流程(以 synchronized 为例)
synchronized 是Java最基础的同步机制,流程最典型,规范写法如下:
明确共享资源(要保护的数据)
- 确定哪些变量/对象会被多个线程同时读写。
- 规范:将这些字段标记为
private,并添加final(如果引用不变)。
选择合适的锁对象
- 实例方法同步:锁是
this(当前实例),适合保护实例级别的状态。 - 静态方法同步:锁是
ClassName.class,适合保护类级别的状态。 - 代码块同步:最推荐,可以指定任意对象作为锁。
- 规范:锁对象应声明为
private final,防止外部访问或篡改。
- 规范:锁对象应声明为
编写同步代码块(核心流程)
标准模板:
public class Counter {
// 1. 共享资源(私有)
private int count = 0;
// 2. 创建一个私有的、最终的锁对象(推荐)
private final Object lock = new Object();
// 3. 所有需要同步访问共享资源的方法,都使用同一个lock
public void increment() {
synchronized (lock) { // 只锁关键操作
count++;
// 可以在这里添加复杂逻辑,但要保持块尽量小
} // 锁自动释放
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
对比错误写法:
// 错误:直接同步整个方法,并且锁的是this,可能被外部篡改
public synchronized void increment() {
// 如果这个方法里还有耗时操作(如IO),锁会占用很久,降低性能
count++;
// 还有一些数据库操作...
}
使用 ReentrantLock 的规范流程(更灵活的场景)
相比于 synchronized,ReentrantLock 支持超时、可中断、公平锁等,流程规范如下:
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class LockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock(); // 非公平锁性能更好
public void safeIncrement() {
// 1. 获取锁(建议用try...finally保证释放)
lock.lock();
try {
count++; // 临界区
} finally {
lock.unlock(); // 2. 必须释放!即使发生异常
}
}
// 更健壮的写法:带超时,避免无限等待
public boolean tryIncrementWithTimeout() {
boolean acquired = false;
try {
// 尝试最多等待100ms获取锁
acquired = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (acquired) {
count++;
return true;
} else {
// 获取锁失败,可以做其他事情或者返回错误
System.out.println("获取锁超时,执行降级逻辑");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
return false;
} finally {
if (acquired) {
lock.unlock(); // 只在获得锁时释放
}
}
}
}
流程”的进阶规范:等待-通知模式 (Wait/Notify)
当一个线程需要满足特定条件(如队列非空)才能继续执行时,规范的同步流程是在循环中等待:
public class BlockingQueue {
private final Object lock = new Object();
private final List<Object> queue = new LinkedList<>();
private final int capacity;
public void put(Object item) throws InterruptedException {
synchronized (lock) {
// 1. 必须用while,不能用if,防止虚假唤醒
while (queue.size() == capacity) {
lock.wait();
}
queue.add(item);
lock.notifyAll(); // 唤醒所有等待的线程
}
}
public Object take() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) { // 再次检查条件
lock.wait();
}
Object item = queue.remove(0);
lock.notifyAll();
return item;
}
}
}
规范要点:
- 必须在
synchronized块内调用wait()/notify()。 - 条件检查用
while而非if。
规范同步流程的“清单”
| 步骤 | 规范要求 | 反例 / 陷阱 |
|---|---|---|
| 识别临界区 | 仅保护会变的共享数据,不要锁方法内部的局部变量 | 锁了整个方法,包括不被共享的局部变量 |
| 选择锁对象 | 使用 private final Object lock 或 this / ClassName.class 并明确其范围 |
使用字符串常量或可能被修改的对象作为锁 |
| 获取锁 | 尽量缩小作用域(synchronized 块),减少锁持有时间 |
在同步块内进行磁盘IO、网络请求等耗时操作 |
| 访问共享资源 | 在临界区内完成所有复合操作(如 i++ 的读取+写入) | 在临界区外读取,临界区内写入(导致原子性破坏) |
| 释放锁 | ReentrantLock 必须放在 finally 块中释放 |
lock.lock() 后不写 finally { unlock() } |
| 多锁顺序 | 所有需要加多个锁的线程,按固定顺序加锁(如按A->B) | 线程1锁A再锁B,线程2锁B再锁A(死锁) |
| 条件等待 | 使用 while(condition) { wait() } 的模式 |
使用 if(condition) { wait() } |
| 优先使用高级工具 | 能用 ConcurrentHashMap、AtomicInteger、CopyOnWriteArrayList 等,就不要自己手写锁 |
“重复造轮子”,性能差且易出错 |
例外情况处理
- 线程中断:当你在同步块内调用
InterruptedException时,规范做法是Thread.currentThread().interrupt()恢复中断状态,而不是吞掉异常。 - 锁的可见性:
synchronized和Lock都保证了进入锁时刷新变量,退出锁时写回。如果不使用同步机制,普通变量可能永远看不到其他线程的修改。 - 异常处理:如果在临界区内抛出非受检异常(如
NullPointerException),“锁”会自动释放(synchronized)或需要在finally中释放(ReentrantLock),务必保证异常情况下锁也能释放。
最后建议:在编写你自己的同步逻辑之前,先看看java.util.concurrent包中是否有现成的工具(Semaphore、CountDownLatch、线程安全的集合等),很多时候,不需要自己手写复杂流程,用官方封装好的并发组件更安全、更高效。