同步与锁
概述
多线程同时访问共享变量时,若不做协调,会出现竞态条件(Race Condition):多个线程交错执行导致结果依赖执行顺序,从而产生错误或数据不一致。同步(Synchronization) 通过锁保证某段代码在同一时刻只被一个线程执行,从而得到正确、可预测的结果,即线程安全。本文介绍 Java 中最常用的两种同步方式:synchronized 关键字与 Lock 显式锁(以 ReentrantLock 为例),并说明使用场景与注意事项。学习前建议先掌握 线程创建与生命周期。
为什么需要同步
当多个线程读写同一份数据时,若不加控制,会出现「读-改-写」被拆散、中间状态被其他线程修改等问题。
竞态条件示例
下面是一个典型的非线程安全示例:多个线程对同一计数器自增,由于自增不是原子操作,最终结果往往小于预期。
public class UnsafeCounter {
private int count = 0;
// 非线程安全:count++ 实际是「读-改-写」三步,可能被其他线程打断
public void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
// 期望 20000,实际往往小于 20000
System.out.println("count = " + counter.getCount());
}
}通过同步,可以保证对 count 的「读-改-写」在获得锁的线程内完整执行,从而得到正确结果。
synchronized:内置锁
synchronized 是 Java 提供的内置同步机制,用法简单,由 JVM 负责加锁与释放。锁的是对象或类,同一时刻只有一个线程能持有该锁。
同步实例方法
在实例方法上加上 synchronized,锁的是当前实例对象(this)。同一实例上的多个 synchronized 实例方法会互斥。
public class SafeCounter {
private int count = 0;
// 同步方法:同一时刻只有一个线程能执行该方法(针对同一 SafeCounter 实例)
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}同步静态方法
在静态方法上加上 synchronized,锁的是当前类的 Class 对象。所有调用该静态方法的线程竞争的是同一把类锁。
public class StaticSyncDemo {
private static int shared = 0;
public static synchronized void add() {
shared++;
}
public static synchronized int getShared() {
return shared;
}
}同步块(指定锁对象)
若只想对方法内部分代码加锁,或希望锁的是指定对象,可使用同步块:synchronized (锁对象) { ... }。锁对象可以是任意对象,通常用专门用于加锁的 Object 或当前实例 this。
public class SyncBlockDemo {
private final Object lock = new Object();
private int value = 0;
public void update(int delta) {
// 仅对共享变量的修改加锁,减少锁持有时间
synchronized (lock) {
value += delta;
}
}
public int getValue() {
synchronized (lock) {
return value;
}
}
}提示
同步块时,锁对象应使用 final 的成员变量或不可变对象,避免锁引用被替换导致不同线程使用不同的锁,失去同步效果。
示例:用 synchronized 修复计数器
public class SafeCounterDemo {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SafeCounterDemo counter = new SafeCounterDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + counter.getCount()); // 稳定输出 20000
}
}Lock 显式锁(ReentrantLock)
从 JDK 1.5 开始,java.util.concurrent.locks 包提供了显式锁接口 Lock 及常用实现 ReentrantLock。与 synchronized 相比,显式锁需要手动 lock() 与 unlock(),且 unlock() 必须放在 finally 中,否则锁可能无法释放。
基本用法
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 中释放,否则异常时可能造成死锁
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}tryLock:非阻塞与超时
Lock 支持 tryLock():若当前锁被占用则立即返回 false,不阻塞;tryLock(long time, TimeUnit unit) 可限时等待。适合「尝试获取锁,拿不到就做别的事」的场景。
import java.util.concurrent.TimeUnit;
public class TryLockDemo {
private final Lock lock = new ReentrantLock();
public void tryDoSomething() {
if (lock.tryLock()) {
try {
// 拿到锁,执行临界区代码
System.out.println("获得锁,执行操作");
} finally {
lock.unlock();
}
} else {
System.out.println("未获得锁,跳过或执行其他逻辑");
}
}
public void tryLockWithTimeout() throws InterruptedException {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
// 在 2 秒内拿到锁则执行
} finally {
lock.unlock();
}
}
}
}注意
InterruptedException 是受检异常。使用 tryLock(long, TimeUnit) 或 lockInterruptibly() 时需处理该异常;在 catch 中通常应恢复中断状态:Thread.currentThread().interrupt()。
synchronized 与 Lock 对比
| 特性 | synchronized | Lock(如 ReentrantLock) |
|---|---|---|
| 使用方式 | 关键字,自动加锁/释放 | 显式 lock() / unlock(),需在 finally 中 unlock |
| 锁的获取 | 拿不到就阻塞等待 | 支持 tryLock、超时、可中断(lockInterruptibly) |
| 公平性 | 不保证公平 | ReentrantLock 可指定公平锁(fair=true) |
| 适用场景 | 简单临界区、代码简洁 | 需要非阻塞、超时、多条件等复杂控制 |
提示
多数简单场景用 synchronized 即可,代码更短、不易漏释锁。需要「尝试获取」「超时」「公平锁」或与 Condition 配合时,再考虑 ReentrantLock。
可重入性
可重入指同一线程可以多次获取同一把锁而不会死锁。Java 的 synchronized 与 ReentrantLock 都是可重入锁:若线程已持有锁,再次进入同一锁保护的代码时,只是增加持有计数,释放时逐层减少。
public class ReentrantDemo {
public synchronized void a() {
System.out.println("a");
b(); // 同一线程再次进入 synchronized,不会阻塞自己
}
public synchronized void b() {
System.out.println("b");
}
}注意事项
注意
- 锁的对象要一致:多线程必须竞争同一把锁才能互斥。例如用 synchronized(lock) 时,所有线程必须使用同一个
lock引用。 - 避免死锁:多个线程互相等待对方持有的锁会死锁。尽量按固定顺序申请多把锁,或使用 tryLock 超时退出。
注意
使用 Lock 时,unlock() 必须放在 finally 中。若在 try 中 return 或抛异常而未 unlock,锁将无法释放,导致其他线程永久阻塞(逻辑上的死锁)。
提示
- 缩小锁的粒度:只对共享数据的访问加锁,锁内尽量少做耗时或阻塞操作,减少竞争。
- 不要锁住可变引用:synchronized(lock) 时,lock 不要换成别的对象;用
final修饰锁引用可避免误改。
相关链接
- 线程创建与生命周期 — 线程的创建与状态
- 线程池 — 使用线程池管理并发任务
- 并发工具类简介 — JUC 中的锁与工具
- Oracle Java 教程 - 同步