Java 同步关键字 synchronized用法 、场景及避坑指南
作者:heartbeat..
Java 同步关键字 synchronized:用法 + 场景 + 避坑指南
一、介绍
在 Java 中,synchronized 是 内置的线程同步机制,用于解决多线程并发访问共享资源时的线程安全问题(如竞态条件、数据不一致)。其核心原理是 通过 “锁” 控制多个线程对共享资源的互斥访问,确保同一时刻只有一个线程能执行临界区代码。
二、主要特点
- 互斥性:同一时刻,只有一个线程能持有锁并执行临界区代码(其他线程需阻塞等待锁释放)。
- 可见性:锁释放时,线程对共享变量的修改会被 “刷新” 到主内存;其他线程获取锁时,会从主内存重新读取变量(避免缓存一致性问题)。
- 有序性:禁止指令重排序(临界区代码在多线程视角下按顺序执行)。
举个例子:
synchronized 就像一个 “共享资源的专属门卫”:
- 它守着一个 “资源入口”(临界区);
- 每个想用资源的 “线程”,都得先跟门卫要 “通行证”(获取锁);
- 门卫只给一个人发通行证,其他人只能在门口排队(阻塞);
- 拿到通行证的人用完资源后,必须把通行证还给门卫(释放锁),下一个排队的人才能拿到;
- 门卫还认人(可重入性):同一个人再要通行证,直接给,不用排队;
- 门卫还能灵活守 “小门”(代码块)或 “大门”(整个方法):守小门效率高,守大门简单但容易堵。
synchronized 的核心作用 ——让多线程 “排队使用共享资源”,避免混乱和冲突。
三、使用场景(3 种形式)
synchronized 可修饰 方法 或 代码块,本质是对 “锁对象” 加锁,锁的粒度决定同步范围。
1. 修饰实例方法(对象锁)
- 锁对象:当前 实例对象(this)。
- 效果:同一实例的多个线程,竞争同一把锁;不同实例的线程互不影响(各自持有自己的锁)。
public class SynchronizedDemo {
// 实例方法加锁:锁是当前对象(this)
public synchronized void instanceMethod() {
// 临界区:共享资源操作(如修改实例变量)
System.out.println(Thread.currentThread().getName() + " 执行实例方法");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
// 两个线程竞争 demo 实例的锁,串行执行
new Thread(demo::instanceMethod, "线程1").start();
new Thread(demo::instanceMethod, "线程2").start(); // 等待线程1释放锁
}
}2. 修饰静态方法(类锁)
- 锁对象:当前类的 Class 对象(类的唯一全局锁)。
- 效果:所有该类的实例(无论多少个对象)共享同一把锁,多线程竞争类锁时串行执行。
public class SynchronizedDemo {
// 静态方法加锁:锁是 SynchronizedDemo.class
public static synchronized void staticMethod() {
System.out.println(Thread.currentThread().getName() + " 执行静态方法");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
public static void main(String[] args) {
SynchronizedDemo demo1 = new SynchronizedDemo();
SynchronizedDemo demo2 = new SynchronizedDemo();
// 两个线程竞争同一把类锁(SynchronizedDemo.class),串行执行
new Thread(demo1::staticMethod, "线程A").start();
new Thread(demo2::staticMethod, "线程B").start(); // 等待线程A释放锁
}
}3. 修饰代码块(显式指定锁对象)
- 锁对象:可自定义(任意 非 null 对象,如
this、Class 对象、自定义对象)。 - 效果:仅对代码块内的逻辑加锁,粒度更细(推荐,减少锁竞争开销)。
public class SynchronizedDemo {
private final Object lock = new Object(); // 自定义锁对象(推荐用 final,避免锁对象被修改)
private int count = 0;
public void syncBlock() {
// 1. 自定义对象锁:锁定 lock 对象
synchronized (lock) {
count++;
System.out.println(Thread.currentThread().getName() + ":count=" + count);
}
// 2. 实例锁(等价于修饰实例方法)
synchronized (this) {
// 临界区逻辑
}
// 3. 类锁(等价于修饰静态方法)
synchronized (SynchronizedDemo.class) {
// 临界区逻辑
}
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
// 多线程竞争 lock 对象,count 自增线程安全
for (int i = 0; i < 3; i++) {
new Thread(demo::syncBlock, "线程" + i).start();
}
}
}举个示例:
假设某电商平台有一款爆款商品,库存仅剩 10 件。同时有 100 个用户抢购(每个用户对应一个线程),每个用户抢购 1 件商品。要求:
- 库存不能为负数;
- 最终卖出的商品数量 = 初始库存(不能多卖,也不能少卖)。
如果不加 synchronized,会出现 “超卖”(比如库存 10,但卖出 12 件);加了 synchronized 后,能保证线程安全,精准扣减库存。
不加synchronized(线程不安全,会超卖)
public class StockDemo {
// 商品初始库存:10件
private int stock = 10;
// 抢购方法(未加锁)
public void buy() {
// 1. 检查库存是否充足
if (stock > 0) {
// 模拟网络延迟(放大线程安全问题)
try { Thread.sleep(10); } catch (InterruptedException e) {}
// 2. 库存扣减
stock--;
// 3. 打印抢购结果
System.out.println(Thread.currentThread().getName() + " 抢购成功!剩余库存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 抢购失败!库存不足");
}
}
public static void main(String[] args) {
StockDemo stock = new StockDemo();
// 100个用户同时抢购(100个线程)
for (int i = 0; i < 100; i++) {
new Thread(stock::buy, "用户" + (i + 1)).start();
}
}
}结果:
用户1 抢购成功!剩余库存:9
用户2 抢购成功!剩余库存:8
...
用户10 抢购成功!剩余库存:0
用户11 抢购成功!剩余库存:-1 // 超卖了!库存为负数
用户12 抢购成功!剩余库存:-2 // 继续超卖
问题原因:多个线程同时进入 if (stock > 0) 判断(比如库存还剩 1 时,5 个线程同时通过判断),之后都执行 stock--,导致库存被多次扣减,出现负数。
加synchronized(线程安全,无超卖)
给 buy() 方法加 synchronized,或给 “库存检查 + 扣减” 的临界区加锁,保证同一时刻只有一个线程能执行核心逻辑:
public class StockDemo {
private int stock = 10;
// 方案1:修饰实例方法(锁对象是当前 StockDemo 实例)
public synchronized void buy() {
if (stock > 0) {
try { Thread.sleep(10); } catch (InterruptedException e) {}
stock--;
System.out.println(Thread.currentThread().getName() + " 抢购成功!剩余库存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 抢购失败!库存不足");
}
}
// 方案2:修饰代码块(锁对象自定义,粒度更细,推荐)
/*
public void buy() {
synchronized (this) { // 锁当前实例,也可以用自定义锁对象(如 private final Object lock = new Object();)
if (stock > 0) {
try { Thread.sleep(10); } catch (InterruptedException e) {}
stock--;
System.out.println(Thread.currentThread().getName() + " 抢购成功!剩余库存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 抢购失败!库存不足");
}
}
}
*/
public static void main(String[] args) {
StockDemo stock = new StockDemo();
// 100个用户同时抢购
for (int i = 0; i < 100; i++) {
new Thread(stock::buy, "用户" + (i + 1)).start();
}
}
}结果:
用户1 抢购成功!剩余库存:9
用户2 抢购成功!剩余库存:8
...
用户10 抢购成功!剩余库存:0
用户11 抢购失败!库存不足
用户12 抢购失败!库存不足
...
用户100 抢购失败!库存不足
扩展:如果是多实例场景?
如果电商平台部署了多个服务实例(每个实例都是一个 StockDemo 对象),此时 synchronized(对象锁 / 类锁)只能保证 “单个实例内的线程安全”,无法跨实例同步(比如实例 1 的库存 10,实例 2 的库存 10,可能导致总超卖 20 件)。
这种情况需要 分布式锁(如 Redis 分布式锁、ZooKeeper 分布式锁),本质是把 “锁” 放到多个实例都能访问的公共地方(如 Redis),实现跨实例的互斥。
但单实例内的并发安全,synchronized 完全能搞定,且简单高效。
四、锁的实现原理(JVM 层面)
synchronized 早期基于 重量级锁(依赖操作系统内核的互斥量 mutex,切换成本高),JDK 6 后引入 锁优化(偏向锁、轻量级锁、重量级锁),通过 对象头(Mark Word) 存储锁状态:
1. 对象头(Mark Word)结构
Java 对象在内存中分为 3 部分:对象头、实例数据、对齐填充。其中 Mark Word 是实现锁的核心,存储内容随锁状态变化:
| 锁状态 | Mark Word 存储内容 |
|---|---|
| 无锁 | 对象哈希码、GC 分代年龄、是否偏向锁(0) |
| 偏向锁 | 偏向线程 ID、GC 分代年龄、是否偏向锁(1) |
| 轻量级锁 | 指向线程栈中锁记录(Lock Record)的指针 |
| 重量级锁 | 指向 JVM 中监视器锁(Monitor)的指针 |
2. 锁升级流程(从低开销到高开销)
JVM 按 “竞争程度” 动态升级锁,避免不必要的性能损耗:
- 偏向锁:单线程场景下,线程获取锁后,Mark Word 记录线程 ID,后续无需再次竞争(直接复用锁),开销极低。
- 轻量级锁:多线程交替执行临界区(无激烈竞争),线程通过 CAS(原子操作) 将 Mark Word 替换为自己的锁记录指针,避免内核态切换。
- 重量级锁:多线程同时竞争锁(激烈竞争),CAS 失败的线程阻塞等待(依赖操作系统 mutex),开销最高。
五、关键特性
- 可重入性:同一线程可多次获取同一把锁(不会死锁)。例如:
public synchronized void methodA() {
methodB(); // 同一线程再次获取当前对象锁,允许执行
}
public synchronized void methodB() {}
- 原理:锁记录中维护 重入计数器,线程首次获取锁时计数器 = 1,再次获取时计数器 + 1,释放时计数器 - 1,直至为 0 释放锁。
- 不可中断性:线程获取锁时(如重量级锁),若锁被占用,线程会进入 BLOCKED 状态,无法被中断(需等待锁释放)。
- 非公平锁:默认是 非公平锁(线程释放锁后,等待队列中的线程和新请求锁的线程竞争,新线程可能 “插队”),无法直接改为公平锁(需借助
ReentrantLock)。
六、与 Lock 接口的对比
synchronized 是 JVM 内置锁,java.util.concurrent.locks.Lock 是 API 层面的锁,核心差异如下:
| 特性 | synchronized | Lock(如 ReentrantLock) |
|---|---|---|
| 锁实现 | JVM 内置(C++ 实现) | Java 代码实现(API 层面) |
| 锁类型 | 非公平锁(默认) | 可公平 / 非公平(构造函数指定) |
| 可中断性 | 不可中断(BLOCKED 状态) | 可中断(lockInterruptibly()) |
| 超时获取 | 无(只能无限等待) | 支持超时获取(tryLock(long, TimeUnit)) |
| 条件变量 | 隐式(通过 wait()/notify()) | 显式(Condition 对象,支持多条件) |
| 锁释放 | 自动释放(方法退出 / 代码块结束) | 必须手动释放(unlock(),需在 finally 中) |
| 性能 | 优化后(偏向 / 轻量级锁)接近 Lock | 高并发下更灵活(如非阻塞获取) |
七、使用注意事项
- 锁粒度不宜过大:避免对整个方法加锁(尤其是包含非临界区逻辑时),优先使用代码块锁定最小临界区(减少锁竞争)。
- 锁对象不可变:代码块锁的对象需用
final修饰(避免锁对象被重新赋值,导致多线程持有不同锁,失去同步效果)。 - 避免死锁:
- 多个线程获取锁的顺序保持一致(如线程 1 先锁 A 再锁 B,线程 2 也先锁 A 再锁 B);
- 避免锁嵌套过深;
- 可使用超时锁(
Lock.tryLock())替代synchronized防止死锁。
- 避免锁竞争激烈场景:
- 高并发下,
synchronized重量级锁的性能较差,可考虑用ConcurrentHashMap等并发容器,或通过分段锁、无锁编程(CAS)优化。
- 高并发下,
八、总结
synchronized 是 Java 最基础、最常用的线程同步方式,优点是 简单易用、无需手动释放锁、JVM 自动优化,适合解决大多数线程安全问题(如简单的共享变量修改、方法同步)。其核心是通过 “锁对象” 实现互斥访问,结合 JDK 6 后的锁升级机制,在低竞争场景下性能优异,高竞争场景下可考虑用 Lock 接口或并发容器替代,若是多实例场景使用分布式锁。
到此这篇关于Java 同步关键字 synchronized:用法 + 场景 + 避坑指南的文章就介绍到这了,更多相关Java 关键字 synchronized内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
