为什么需要 synchronized?—— 线程安全问题
在多线程环境下,当多个线程同时访问和修改同一个共享资源(如一个变量、一个对象)时,可能会发生不可预期的结果,这就是线程安全问题。

核心原因:
- 原子性:一个操作(如
i++)在底层可能不是一条指令,而是“读取-修改-写入”三步,如果两个线程同时执行,可能会导致最终结果不正确。 - 可见性:一个线程对共享变量的修改,可能不会立即对其他线程可见,每个线程都有自己的工作内存,变量的值可能被缓存。
synchronized 的作用:
通过一种互斥锁的机制,确保在同一时间,只有一个线程可以进入被 synchronized 保护的代码块或方法,从而解决了上述的原子性和可见性问题。
synchronized 的两种用法
synchronized 可以用在两个地方:
- 实例方法
- 静态方法
- 代码块
代码块又分为:

- 对当前实例对象加锁(
this) - 对指定对象加锁
- 对 Class 对象加锁(用于静态方法和静态代码块)
下面我们逐一讲解。
synchronized 详细用法解析
A. synchronized 实例方法
当 synchronized 修饰一个非静态方法时,锁的是当前对象实例。
public class Counter {
private int count = 0;
// 锁的是 this 对象,即 Counter 的实例
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
工作原理:
- 线程 A 调用
obj.increment(),它会获取obj这个对象的锁。 - 在线程 A 释放锁之前(方法执行完毕或抛出异常),其他任何线程如果再调用
obj.increment()或其他任何以obj为锁的synchronized方法,都会被阻塞,必须等待。 - 锁是针对对象的,而不是针对代码的,同一个对象的不同
synchronized方法共享同一把锁。
B. synchronized 静态方法
当 synchronized 修饰一个静态方法时,锁的是当前类的 Class 对象。
public class Counter {
private static int staticCount = 0;
// 锁的是 Counter.class 对象
public synchronized static void staticIncrement() {
staticCount++;
}
}
工作原理:
- 线程 A 调用
Counter.staticIncrement(),它会获取Counter.class这个 Class 对象的锁。 - 在线程 A 释放锁之前,其他任何线程调用
Counter.staticIncrement()都会被阻塞。 - 重要区别:静态方法的锁是类级别的,无论你创建了多少个
Counter实例(new Counter()),它们共享同一个Counter.class对象,所有线程在访问静态synchronized方法时,都在竞争同一把锁。
C. synchronized 代码块
这是 synchronized 最灵活、也最推荐的方式,因为它可以精确地锁定需要同步的代码范围,而不是整个方法。
锁定当前实例对象 (this)
public class Counter {
private int count = 0;
public void increment() {
// 只锁定这一小段代码,而不是整个方法
synchronized (this) {
count++;
}
}
}
这和 synchronized 实例方法的效果完全一样,但粒度更细。
锁定指定对象
你可以指定任意一个对象作为锁,这提供了极大的灵活性。
public class SharedObject {
private int value = 0;
private final Object lock = new Object(); // 专门用于同步的锁对象
public void setValue(int value) {
synchronized (lock) { // 锁定的是 lock 对象
this.value = value;
}
}
public int getValue() {
synchronized (lock) { // 同一把锁
return this.value;
}
}
}
最佳实践:
- 不要用字符串(
String)或基本类型包装类(如Integer)作为锁,因为它们可能在 JVM 内部被重用或缓存,导致意想不到的锁竞争。 - 通常使用一个专门声明为
private final的对象作为锁,这能确保锁的唯一性和可控性。
锁定 Class 对象
对于需要同步静态资源的情况,使用代码块锁定 ClassName.class 是一个好选择。
public class Counter {
private static int staticCount = 0;
public static void staticIncrement() {
synchronized (Counter.class) { // 锁定的是 Counter.class
staticCount++;
}
}
}
这和 synchronized 静态方法的效果完全一样,但同样提供了更细的粒度。
synchronized 的底层原理(JDK 1.6 优化)
在早期版本,synchronized 是一个重量级锁,性能较差,但从 JDK 1.6 开始,JVM 对其进行了大量优化,引入了锁升级机制,使其性能大幅提升。
锁的状态分为四种,会随着竞争情况逐渐升级:
- 无锁状态
- 偏向锁:假设锁总是由同一个线程获取,当一个线程获取锁时,JVM 会在对象头里记录这个线程的 ID,之后这个线程再获取锁时,无需任何 CAS 操作,直接进入,速度非常快,适用于低竞争场景。
- 轻量级锁:当有另一个线程来竞争偏向锁时,偏向锁会升级为轻量级锁,竞争线程会通过自旋 的方式尝试获取锁,而不是立即阻塞,自旋就是在一个循环中不断检查锁是否被释放,适用于竞争短暂的场景。
- 重量级锁:如果自旋一定次数后仍然没有获取到锁,或者自旋线程数过多,锁就会升级为重量级锁,未获取到锁的线程会被阻塞,并进入等待队列,由操作系统内核进行调度,这是传统的锁机制,性能最差,但能保证公平性。
这个锁升级的过程是不可逆的,只能从低级到高级。
synchronized 与 java.util.concurrent.locks.Lock 的对比
| 特性 | synchronized |
java.util.concurrent.locks.Lock (如 ReentrantLock) |
|---|---|---|
| 实现 | JVM 关键字,是 Java 语法层面的内置特性 | API 层面的实现,是一个接口 |
| 锁的获取 | 隐式获取,代码块或方法执行完毕自动释放 | 显式获取(lock())和释放(unlock()),通常在 finally 块中释放 |
| 锁类型 | 非公平锁(JDK 1.8 及以后) | 可以是公平锁(new ReentrantLock(true))或非公平锁(默认) |
| 功能 | 功能简单,只提供基本的互斥 | 功能强大,支持可中断的锁获取、超时获取锁、多条件变量(Condition)等 |
| 性能 | JDK 1.6 后优化良好,在低竞争下性能优异 | 在高竞争下,ReentrantLock 的性能通常优于 synchronized |
| 使用建议 | 优先使用 synchronized,代码简洁,不易出错,在大多数场景下已经足够。 |
当需要 synchronized 无法提供的高级功能时(如公平锁、多条件变量),才考虑使用 Lock。 |
总结与最佳实践
- 理解核心:
synchronized通过互斥锁保证线程安全,解决了原子性和可见性问题。 - 掌握用法:
- 实例方法:锁
this。 - 静态方法:锁
Class对象。 - 代码块:最灵活,可以锁任意对象,推荐锁定一个专门的
private final对象。
- 实例方法:锁
- 遵循原则:
- 尽量减小同步范围:只在必要时同步,同步范围越小越好,优先使用
synchronized代码块而不是整个方法。 - 避免锁竞争:高并发下,
synchronized可能会成为性能瓶颈,考虑优化算法或使用并发集合(如ConcurrentHashMap)。 - 不要用
String或基本类型包装类做锁。 - 始终在
finally功块中释放锁:对于Lock接口这是必须的,对于synchronized则是自动的,无需手动操作。
- 尽量减小同步范围:只在必要时同步,同步范围越小越好,优先使用
- 首选
synchronized:除非你有明确的、需要ReentrantLock等高级功能的理由,否则在并发编程中,synchronized应该是你的首选,它简单、可靠,JVM 对它的优化已经非常成熟。
