并发编程(六):synchronized :从使用误区到锁升级
·
在前一篇内容中,我们从 CPU 缓存原理讲清了可见性、有序性问题,也知道volatile能解决这两个问题,但它有个致命短板 —— 无法保证原子性。而在并发编程中,原子性是解决多线程操作同一变量的核心诉求,这时候就需要 synchronized 登场。
一、synchronized 的 3 个使用误区
误区 1:分开加锁 = 没加锁
原则:需要保证原子性的完整操作,必须全部包裹在同一把锁内,操作没完成不释放锁。如果把一个原子操作拆成多段、分开加锁,中间的无锁区间会让并发问题卷土重来。
错误示范:
public class WrongSyncDemo {
private int count = 0;
// 错误:把count++拆成"读→计算→写"三段,仅给读和写单独加锁
public void wrongAdd() {
// 第一步:加锁读
synchronized (this) {
count = count; // 仅演示读操作
}
// 无锁区间:其他线程可修改count,产生脏数据
int temp = count + 1;
// 第二步:加锁写
synchronized (this) {
count = temp;
}
}
public static void main(String[] args) throws InterruptedException {
WrongSyncDemo demo = new WrongSyncDemo();
// 两个线程各执行10000次加操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) demo.wrongAdd();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) demo.wrongAdd();
});
t1.start();
t2.start();
t1.join();
t2.join();
// 结果远小于20000,并发问题依然存在
System.out.println("最终count值:" + demo.count);
}
}
完整操作加锁
public class CorrectSyncDemo {
private int count = 0;
// 正确:把"读→计算→写"完整操作包裹在同一把锁内
public void correctAdd() {
synchronized (this) { // 加锁后,直到代码块结束才释放
count++; // 三步原子完成,无中间间隙
}
}
public static void main(String[] args) throws InterruptedException {
CorrectSyncDemo demo = new CorrectSyncDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) demo.correctAdd();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) demo.correctAdd();
});
t1.start();
t2.start();
t1.join();
t2.join();
// 结果精准等于20000,原子性得到保证
System.out.println("最终count值:" + demo.count);
}
}
误区 2:误以为非静态同步方法只锁 “当前方法”
非静态synchronized方法的锁是当前实例对象(this),而非某个方法。
- 对同一个实例:只要有一个非静态同步方法被锁住,该实例的所有非静态同步方法都会被阻塞(“锁住一个,锁住全部”)
- 对不同实例:每个实例有独立的锁,互不影响
代码验证
public class InstanceLockDemo {
// 非静态同步方法1
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " 进入m1");
try {
Thread.sleep(3000); // 模拟长时间操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 退出m1");
}
// 非静态同步方法2
public synchronized void m2() {
System.out.println(Thread.currentThread().getName() + " 进入m2");
}
// 非同步方法(不受锁影响)
public void m3() {
System.out.println(Thread.currentThread().getName() + " 进入m3");
}
public static void main(String[] args) {
InstanceLockDemo demo = new InstanceLockDemo();
// 线程1调用m1,占用demo实例锁
new Thread(() -> demo.m1(), "线程1").start();
// 线程2调用m2,需等待线程1释放demo锁
new Thread(() -> demo.m2(), "线程2").start();
// 线程3调用m3,不受锁影响,立即执行
new Thread(() -> demo.m3(), "线程3").start();
}
}
输出结果
线程1 进入m1
线程3 进入m3
线程1 退出m1
线程2 进入m2
误区 3:误以为静态同步方法的锁和非静态同步方法共用
静态synchronized方法的锁是类的 Class 对象(存储在 JVM 方法区,整个类只有 1 份),而非实例对象。
- 静态同步方法的 “类锁” 和非静态同步方法的 “实例锁” 完全独立,互不干扰
- 对同一个类:只要有一个静态同步方法被锁住,该类的所有静态同步方法都会被阻塞
- 不加锁的静态方法、非静态方法,不受类锁影响
代码验证
public class ClassLockDemo {
// 静态同步方法1
public static synchronized void staticM1() {
System.out.println(Thread.currentThread().getName() + " 进入staticM1");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 退出staticM1");
}
// 静态同步方法2
public static synchronized void staticM2() {
System.out.println(Thread.currentThread().getName() + " 进入staticM2");
}
// 非静态同步方法(锁为实例对象,与类锁独立)
public synchronized void instanceM() {
System.out.println(Thread.currentThread().getName() + " 进入instanceM");
}
public static void main(String[] args) {
ClassLockDemo demo = new ClassLockDemo();
// 线程1调用静态同步方法,占用Class锁
new Thread(() -> ClassLockDemo.staticM1(), "线程1").start();
// 线程2调用静态同步方法,需等待Class锁释放
new Thread(() -> ClassLockDemo.staticM2(), "线程2").start();
// 线程3调用非静态同步方法,用的是实例锁,立即执行
new Thread(() -> demo.instanceM(), "线程3").start();
}
}
执行结果:
线程1 进入staticM1
线程3 进入instanceM
线程1 退出staticM1
线程2 进入staticM2
二、synchronized 的锁升级机制
很多人觉得synchronized是 “重量级锁”,但 Java 1.6 后 JVM 对其做了优化 —— 引入锁升级,让大部分场景下的synchronized性能接近无锁。
锁升级的逻辑:从偏向锁→轻量级锁→重量级锁,根据竞争激烈程度逐步升级,避免一开始就使用低效的重量级锁。
1. 锁的存储位置:对象头(Mark Word)
JVM 中每个对象的头部(Mark Word)会存储锁的状态、持有线程 ID 等信息,锁升级的本质就是修改对象头的标记。
2. 锁升级流程
| 锁类型 | 适用场景 | 核心逻辑 |
|---|---|---|
| 偏向锁 | 无竞争 / 单线程重复加锁 | 给对象头标记 “偏向线程 ID”,后续该线程加锁无需 CAS,直接获取(几乎无开销) |
| 轻量级锁 | 轻度竞争(线程交替加锁) | 无竞争线程尝试用 CAS 修改对象头获取锁,失败则自旋等待(避免内核态切换) |
| 重量级锁 | 重度竞争(线程同时加锁) | 自旋多次失败后,升级为重量级锁,依赖操作系统互斥量(会触发上下文切换) |
3. 结论
- 锁升级是单向的(偏向锁→轻量级锁→重量级锁),不会降级
- 大部分业务场景中,
synchronized停留在偏向锁 / 轻量级锁阶段,性能并不差- 只有高并发、激烈竞争的场景下,才会升级为重量级锁,产生明显性能开销
三、ynchronized 该怎么用?
- 锁粒度要精准:只锁需要保证原子性的代码块,而非整个方法(比如转账业务,只锁 “扣减 A 账户 + 增加 B 账户” 的代码块,而非整个转账方法)
- 锁对象要选对:
- 实例相关的共享变量:用实例锁(非静态同步方法 /
synchronized(this)) - 类相关的共享变量:用类锁(静态同步方法 /
synchronized(XXX.class))
- 实例相关的共享变量:用实例锁(非静态同步方法 /
- 避免锁竞争:尽量让锁停留在偏向锁 / 轻量级锁阶段(比如减少多线程同时加锁、避免锁持有时间过长)
- 不要滥用锁:如果只是保证可见性,用
volatile即可;只有需要保证原子性时,再用synchronized
总结
synchronized的核心是保证原子性,使用时必须把完整操作包裹在同一把锁内,分开加锁等于没加锁- 非静态同步方法锁的是实例对象,静态同步方法锁的是 Class 对象,两类锁相互独立
- 实际需精准控制锁粒度和锁对象,根据竞争程度选择合适的同步方案
更多推荐



所有评论(0)