synchronized关键字深度剖析与实战
深入探讨Java synchronized关键字的核心原理、Monitor机制和锁优化策略,结合实际项目经验分享面试要点和性能优化方案。涵盖偏向锁、轻量级锁、重量级锁的完整升级过程。
🤔 问题背景与技术演进
我们要解决什么问题?
在多线程编程中,线程安全问题是核心挑战。当多个线程同时访问共享资源时,会出现以下关键问题:
- 竞态条件:程序的最终结果依赖于线程执行的时序,导致结果不可预测
- 数据不一致:多个线程同时修改共享变量,造成数据状态混乱
- 原子性问题:复合操作无法保证一次性完成,中间状态可能被其他线程观察到
// 典型的线程安全问题示例
public class CounterProblem {
private int count = 0;
// 非线程安全的递增操作
public void increment() {
count++; // 这实际上是三个步骤:读取、计算、写入
}
}
没有这个技术时是怎么做的?
在synchronized关键字出现之前,Java开发者主要依靠以下方式处理并发问题:
1. 单线程编程
- 完全避免多线程,程序按顺序执行
- 问题:无法充分利用多核CPU性能,响应性差
2. 手工同步机制
- 使用复杂的标志位和轮询机制
- 通过volatile变量和while循环实现互斥
- 问题:代码复杂、容易出错、性能低下
3. 操作系统级别的互斥量
- 直接调用操作系统提供的互斥原语
- 问题:平台相关、使用复杂、难以调试
技术演进的历史脉络
JDK 1.0 (1996):synchronized关键字首次引入
- 基于Monitor概念实现
- 提供简单易用的同步机制
- 性能较差,所有同步都是重量级操作
JDK 1.6 (2006):锁优化的重大突破
- 引入偏向锁(Biased Locking)
- 实现轻量级锁(Lightweight Locking)
- 锁消除和锁粗化优化
JDK 1.8及以后:持续优化
- 改善锁竞争检测算法
- 优化Monitor实现
- 与其他并发工具形成完整体系
🎯 核心概念与原理
基础概念定义
Synchronized是Java提供的内置同步机制,用于确保同一时刻只有一个线程能够执行被保护的代码段。它基于**对象监视器(Object Monitor)**的概念实现。
核心术语:
- Monitor:每个Java对象都有一个关联的监视器
- 临界区:被synchronized保护的代码区域
- 锁对象:提供监视器的对象实例
工作原理详解
synchronized的核心工作机制基于Monitor模式:
// 同步方法
public synchronized void syncMethod() {
// 临界区代码
}
// 同步代码块
public void syncBlock() {
synchronized(this) { // 获取this对象的监视器
// 临界区代码
} // 自动释放监视器
}
执行流程:
- 进入监视器:线程尝试获取对象的监视器锁
- 执行临界区:成功获取锁的线程执行同步代码
- 释放监视器:代码执行完毕或异常退出时自动释放锁
- 唤醒等待线程:其他等待的线程被唤醒重新竞争
技术特点和优势
synchronized的核心特性:
- 互斥性:确保同一时刻只有一个线程访问
- 可见性:保证线程间的内存可见性
- 可重入性:同一线程可以多次获取同一把锁
- 自动释放:异常或正常退出都会自动释放锁
相比手工同步的优势:
- 语法简洁,易于使用和理解
- 自动管理锁的获取和释放
- JVM层面优化,性能不断改善
- 与Java异常处理机制良好集成
🔧 实现原理与源码分析
底层实现机制
synchronized在字节码层面通过monitorenter和monitorexit指令实现:
public void syncMethod() {
synchronized(lock) {
count++;
}
}
对应的字节码:
monitorenter // 获取监视器锁
aload_0 // 加载this引用
getfield // 获取count字段
aload_0 // 加载this引用
dup // 复制引用
getfield // 获取count字段
iconst_1 // 常量1
iadd // 执行加法
putfield // 存储结果
monitorexit // 释放监视器锁
锁升级机制详解
锁升级的渐进策略: synchronized采用渐进式锁升级的设计思想,根据竞争程度动态调整:
/**
* 锁升级演示
*/
public class LockUpgradeDemo {
private int count = 0;
public synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
LockUpgradeDemo demo = new LockUpgradeDemo();
// 阶段1:偏向锁(单线程访问)
System.out.println("=== 偏向锁阶段 ===");
for (int i = 0; i < 1000; i++) {
demo.increment();
}
// 阶段2:轻量级锁(少量线程交替)
System.out.println("=== 轻量级锁阶段 ===");
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
demo.increment();
}
});
t1.start();
t1.join();
System.out.println("最终结果: " + demo.count);
}
}
三种锁状态详解:
偏向锁:只有一个线程访问同步代码
- 在对象头记录线程ID
- 后续访问直接检查线程ID,无需CAS操作
- 适合单线程重复进入的场景
轻量级锁:多个线程交替访问,无激烈竞争
- 使用CAS操作获取锁
- 在栈帧中存储锁记录
- 适合线程交替执行的场景
重量级锁:多个线程激烈竞争
- 基于操作系统互斥量实现
- 线程会被阻塞和唤醒
- 适合高竞争的并发场景
💡 实战案例与代码示例
具体项目应用
场景:电商系统库存扣减
在电商系统中,库存扣减是典型的并发场景。多个用户同时购买商品时,必须确保库存数据的一致性。
/**
* 电商库存管理系统
*/
@Service
public class InventoryService {
// 商品库存映射
private final Map<Long, Integer> inventory = new ConcurrentHashMap<>();
// 为每个商品创建独立的锁对象,减少锁粒度
private final Map<Long, Object> productLocks = new ConcurrentHashMap<>();
/**
* 扣减库存(线程安全版本)
*/
public boolean decreaseStock(Long productId, int quantity) {
// 获取商品专用锁
Object lock = productLocks.computeIfAbsent(productId, k -> new Object());
synchronized (lock) {
Integer currentStock = inventory.get(productId);
// 检查库存是否充足
if (currentStock == null || currentStock < quantity) {
return false; // 库存不足
}
// 扣减库存
inventory.put(productId, currentStock - quantity);
// 记录库存变化日志(简化)
logStockChange(productId, -quantity, currentStock - quantity);
return true;
}
}
/**
* 恢复库存(退货场景)
*/
public void increaseStock(Long productId, int quantity) {
Object lock = productLocks.computeIfAbsent(productId, k -> new Object());
synchronized (lock) {
Integer currentStock = inventory.getOrDefault(productId, 0);
inventory.put(productId, currentStock + quantity);
logStockChange(productId, quantity, currentStock + quantity);
}
}
/**
* 查询库存(只读操作,无需同步)
*/
public Integer getStock(Long productId) {
return inventory.get(productId);
}
private void logStockChange(Long productId, int change, int newStock) {
System.out.printf("商品%d库存变化: %+d, 当前库存: %d%n",
productId, change, newStock);
}
}
完整代码实现
生产者消费者模式实现:
/**
* 基于synchronized的生产者消费者实现
*/
public class ProducerConsumerDemo {
private final Object lock = new Object();
private final Queue<String> buffer = new LinkedList<>();
private final int capacity = 10;
/**
* 生产者
*/
class Producer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
produce("Item-" + i);
Thread.sleep(100); // 模拟生产时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
private void produce(String item) throws InterruptedException {
synchronized (lock) {
// 等待缓冲区有空间
while (buffer.size() >= capacity) {
System.out.println("缓冲区已满,生产者等待...");
lock.wait();
}
// 生产商品
buffer.offer(item);
System.out.println("生产: " + item + ", 缓冲区大小: " + buffer.size());
// 通知消费者
lock.notifyAll();
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
private final String name;
public Consumer(String name) {
this.name = name;
}
@Override
public void run() {
try {
while (true) {
String item = consume();
if (item == null) break; // 没有更多商品
// 模拟消费时间
Thread.sleep(150);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private String consume() throws InterruptedException {
synchronized (lock) {
// 等待缓冲区有商品
while (buffer.isEmpty()) {
System.out.println(name + " 等待商品...");
lock.wait(1000); // 超时等待
if (buffer.isEmpty()) {
return null; // 超时退出
}
}
// 消费商品
String item = buffer.poll();
System.out.println(name + " 消费: " + item + ", 缓冲区大小: " + buffer.size());
// 通知生产者
lock.notifyAll();
return item;
}
}
}
}
最佳实践总结
1. 锁粒度控制
// ❌ 错误:锁粒度过粗
public synchronized void processAllUsers() {
for (User user : users) {
updateUser(user); // 整个循环都被锁住
}
}
// ✅ 正确:锁粒度细化
public void processAllUsers() {
for (User user : users) {
synchronized(user) { // 只锁定单个用户
updateUser(user);
}
}
}
2. 避免锁嵌套
// ❌ 错误:可能导致死锁
public synchronized void methodA() {
synchronized(lockB) {
// 业务逻辑
}
}
// ✅ 正确:统一加锁顺序
public void methodA() {
synchronized(lockA) {
synchronized(lockB) {
// 业务逻辑
}
}
}
🎯 面试高频问题精讲
核心面试问题解析
1. synchronized的实现原理是什么?
标准答案: synchronized基于JVM的Monitor机制实现。在字节码层面,同步代码块使用monitorenter和monitorexit指令,同步方法通过方法访问标志ACC_SYNCHRONIZED实现。
扩展要点:
- 每个Java对象都有一个关联的Monitor
- Monitor包含owner、count、waiters等关键字段
- 通过CAS操作和操作系统互斥量保证原子性
面试技巧:结合具体的字节码示例说明,展现对底层机制的深入理解。
2. synchronized的锁升级过程是怎样的?
标准答案: JDK 1.6引入了锁升级机制:偏向锁 → 轻量级锁 → 重量级锁
- 偏向锁:只有一个线程访问,在对象头记录线程ID
- 轻量级锁:多个线程交替访问,使用CAS操作
- 重量级锁:多个线程激烈竞争,使用操作系统互斥量
扩展要点:
- 锁升级是不可逆的(除了GC时重置)
- 升级条件:竞争程度达到阈值
- 每种锁都有其适用场景
3. synchronized和volatile的区别?
标准答案:
特性 | synchronized | volatile |
---|---|---|
原子性 | ✅ 保证 | ❌ 不保证 |
可见性 | ✅ 保证 | ✅ 保证 |
有序性 | ✅ 保证 | ✅ 保证 |
阻塞性 | 会阻塞 | 不会阻塞 |
适用场景 | 复合操作 | 状态标志 |
4. synchronized和ReentrantLock的区别?
标准答案:
- 获取方式:synchronized自动获取,ReentrantLock手动获取
- 释放方式:synchronized自动释放,ReentrantLock需要手动释放
- 中断性:synchronized不可中断,ReentrantLock可中断
- 公平性:synchronized非公平,ReentrantLock可选择
- 性能:高并发下ReentrantLock性能更好
5. 什么是锁消除和锁粗化?
标准答案:
- 锁消除:JIT编译器检测到不可能存在竞争的同步代码,自动消除锁
- 锁粗化:将多个连续的同步操作合并为一个更大范围的同步
// 锁消除示例
public void method() {
StringBuffer sb = new StringBuffer(); // 局部变量,无竞争
sb.append("a"); // 锁会被消除
sb.append("b"); // 锁会被消除
}
// 锁粗化示例
for (int i = 0; i < 1000; i++) {
synchronized(lock) { // 会被粗化为一个大锁
// 操作
}
}
技术对比类问题
synchronized vs. 其他同步机制选择:
- 简单互斥:优先使用synchronized
- 需要超时:使用ReentrantLock
- 读多写少:使用ReadWriteLock
- 无锁编程:使用Atomic类
- 异步编程:使用CompletableFuture
⚡ 性能优化与注意事项
性能瓶颈分析
synchronized的性能开销主要来源:
/**
* 性能测试对比
*/
public class SynchronizedPerformanceTest {
private volatile int volatileCounter = 0;
private int syncCounter = 0;
private AtomicInteger atomicCounter = new AtomicInteger(0);
public synchronized void syncIncrement() {
syncCounter++;
}
public void atomicIncrement() {
atomicCounter.incrementAndGet();
}
// 性能测试方法
public static void performanceTest() {
SynchronizedPerformanceTest test = new SynchronizedPerformanceTest();
int iterations = 1000000;
// 测试synchronized
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
test.syncIncrement();
}
long syncTime = System.nanoTime() - start;
// 测试AtomicInteger
start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
test.atomicIncrement();
}
long atomicTime = System.nanoTime() - start;
System.out.println("Synchronized: " + syncTime / 1000000 + "ms");
System.out.println("AtomicInteger: " + atomicTime / 1000000 + "ms");
}
}
优化策略方案
1. 减少同步范围
// ❌ 同步范围过大
public synchronized void process() {
String data = prepareData(); // 耗时操作
updateSharedResource(data); // 真正需要同步的操作
sendNotification(); // 耗时操作
}
// ✅ 最小化同步范围
public void process() {
String data = prepareData(); // 不需要同步
synchronized(this) { // 只同步必要部分
updateSharedResource(data);
}
sendNotification(); // 不需要同步
}
2. 使用不同的锁对象
public class OptimizedLocking {
private final Object readLock = new Object();
private final Object writeLock = new Object();
private volatile String data = "";
public String readData() {
synchronized(readLock) {
return data;
}
}
public void writeData(String newData) {
synchronized(writeLock) {
this.data = newData;
}
}
}
常见坑点规避
1. 死锁预防
/**
* 死锁预防示例
*/
public class DeadlockPrevention {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
// ✅ 正确:统一加锁顺序
public void method1() {
synchronized(lock1) {
synchronized(lock2) {
// 业务逻辑
}
}
}
public void method2() {
synchronized(lock1) { // 相同的加锁顺序
synchronized(lock2) {
// 业务逻辑
}
}
}
}
2. 字符串常量锁的坑
// ❌ 危险:字符串常量可能被其他代码锁定
synchronized("LOCK_STRING") {
// 可能产生意外的锁竞争
}
// ✅ 安全:使用专用的锁对象
private static final Object LOCK = new Object();
synchronized(LOCK) {
// 安全的同步
}
📚 总结与技术对比
核心要点回顾
- synchronized是Java内置的同步机制,基于Monitor模式实现
- 锁升级机制提升了性能:偏向锁→轻量级锁→重量级锁
- 使用简单但功能有限,适合基础的同步需求
- JVM层面优化包括锁消除、锁粗化等技术
- 正确使用需要注意锁粒度、死锁预防等问题
与相关技术对比
同步机制 | 性能 | 功能 | 使用复杂度 | 适用场景 |
---|---|---|---|---|
synchronized | 中等 | 基础 | 简单 | 基本同步需求 |
ReentrantLock | 高 | 丰富 | 中等 | 复杂同步场景 |
ReadWriteLock | 高 | 专用 | 中等 | 读多写少 |
StampedLock | 最高 | 高级 | 复杂 | 高性能读写 |
Atomic类 | 最高 | 专用 | 简单 | 无锁编程 |
持续学习建议
- 深入JUC包:学习ReentrantLock、Semaphore等高级同步工具
- 理解JVM优化:研究HotSpot的锁优化策略
- 实践项目应用:在实际项目中积累同步设计经验
- 关注新特性:跟踪新版本JDK的并发改进
- 学习无锁编程:掌握CAS和原子操作的使用
下一篇预告:《volatile关键字与内存可见性》将深入探讨Java内存模型、happens-before关系和指令重排序的核心机制。