Java中的同步与锁机制是为了解决多线程并发访问共享资源时可能出现的数据不一致、逻辑错误等问题而设计的。当多个线程同时操作同一个共享变量或者调用同一个对象的方法时,如果没有适当的同步控制,就可能出现线程安全问题,比如计数错误、数据覆盖等。

为什么需要同步与锁
多线程的执行是并发的,线程的执行顺序由操作系统调度决定,我们无法预知线程的执行时序。举一个简单的例子,两个线程同时对一个初始值为0的变量做加1操作,理想情况下最终结果是2,但实际运行可能出现结果为1的情况。
这是因为加1操作不是原子操作,它分为三步:读取当前值、计算加1后的值、写回新值。如果两个线程同时读取到初始值0,各自计算后得到1,再先后写回,最终变量值就是1,而不是预期的2。同步与锁的作用就是保证同一时间只有一个线程能执行这段临界区代码,避免这种竞争问题。
内置锁synchronized
synchronized是Java中最基础的同步机制,它依赖对象的内部锁(也叫监视器锁)实现,是隐式锁,不需要开发者手动释放。
synchronized的使用方式
synchronized可以修饰三种场景:
- 修饰实例方法:锁是当前实例对象,同一个实例的多个线程调用该方法时会互斥
- 修饰静态方法:锁是当前类的Class对象,所有该类的实例调用该方法时都会互斥
- 修饰代码块:锁是括号里配置的对象,灵活性更高,可以缩小锁的范围
synchronized代码示例
下面是一个用synchronized修饰实例方法解决计数问题的例子:
public class Counter {
private int count = 0;
// 修饰实例方法,锁是当前Counter实例
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建两个线程同时执行加1操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
// 等待两个线程执行完成
t1.join();
t2.join();
// 输出结果,预期是2000
System.out.println("最终计数结果:" + counter.getCount());
}
}如果使用synchronized代码块,还可以指定锁对象,减少锁的粒度,提升性能:
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
// 指定锁对象为lock,只有获取lock锁的线程才能执行代码块内容
synchronized (lock) {
count++;
}
}
}显式锁ReentrantLock
ReentrantLock是Java.util.concurrent.locks包下的显式锁,需要开发者手动调用lock()方法获取锁,调用unlock()方法释放锁,通常配合try-finally使用,避免异常情况下锁无法释放。
ReentrantLock的基本使用
ReentrantLock是可重入锁,同一个线程可以多次获取同一把锁,不会造成死锁。它相比synchronized的优势在于:可以尝试获取锁、可以设置超时时间、可以响应中断,功能更灵活。
下面是ReentrantLock的使用示例:
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
// 确保锁一定会被释放
lock.unlock();
}
}
public int getCount() {
return count;
}
}ReentrantLock的尝试获取锁
ReentrantLock提供了tryLock()方法,可以尝试获取锁,如果获取失败不会阻塞当前线程,适合需要避免线程长时间阻塞的场景:
import java.util.concurrent.locks.ReentrantLock;
public class TaskProcessor {
private final ReentrantLock lock = new ReentrantLock();
public void process() {
// 尝试获取锁,获取失败则执行其他逻辑
if (lock.tryLock()) {
try {
System.out.println("获取锁成功,执行任务");
// 模拟任务执行
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败,执行备用逻辑");
}
}
}volatile关键字的作用
volatile是轻量级的同步机制,它只能保证变量的可见性和禁止指令重排序,不能保证原子性,所以不适合处理像计数这种需要多个步骤的复合操作。
可见性指的是当一个线程修改了volatile变量的值,其他线程可以立即看到修改后的值,避免线程使用本地缓存的旧值。比如下面的例子:
public class FlagController {
// 用volatile修饰,保证可见性
private volatile boolean running = true;
public void start() {
new Thread(() -> {
while (running) {
// 执行循环任务
}
System.out.println("线程停止");
}).start();
}
public void stop() {
running = false;
}
}如果不加volatile,子线程可能会一直读取本地缓存的running值,即使主线程修改了running为false,子线程也不会停止循环。
synchronized与ReentrantLock的对比
我们可以通过下面的表格对比两种锁的特点:
| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 锁的类型 | 隐式锁,自动释放 | 显式锁,手动释放 |
| 功能丰富度 | 功能简单,仅支持互斥同步 | 支持尝试获取锁、超时、中断响应等 |
| 性能 | JDK1.6后优化,和ReentrantLock差距很小 | 高竞争场景下性能稍好 |
| 适用场景 | 简单的同步场景,代码简洁优先 | 需要高级锁功能的复杂场景 |
同步与锁的使用注意事项
- 锁的范围要尽可能小,只包裹临界区代码,避免锁粒度过大导致性能下降
- 不要使用字符串常量、Integer等包装类对象作为synchronized的锁对象,因为这些对象可能被多处引用,导致意外的锁竞争
- 使用ReentrantLock时一定要在finally块中释放锁,避免异常导致锁泄漏
- volatile不能替代锁,它只适合单个变量的可见性场景,无法处理复合操作的原子性
理解同步与锁机制的核心是明确线程安全问题的根源,根据实际场景选择合适的同步方案,才能在保障线程安全的同时,最大程度减少性能损耗。
synchronizedReentrantLockvolatile线程安全修改时间:2026-06-06 07:24:13