Gerrad Zhang

synchronized关键字深度剖析与实战

深入探讨Java synchronized关键字的核心原理、Monitor机制和锁优化策略,结合实际项目经验分享面试要点和性能优化方案。涵盖偏向锁、轻量级锁、重量级锁的完整升级过程。

Gerrad Zhang
武汉,中国
2 min read

🤔 问题背景与技术演进

我们要解决什么问题?

在多线程编程中,线程安全问题是核心挑战。当多个线程同时访问共享资源时,会出现以下关键问题:

  • 竞态条件:程序的最终结果依赖于线程执行的时序,导致结果不可预测
  • 数据不一致:多个线程同时修改共享变量,造成数据状态混乱
  • 原子性问题:复合操作无法保证一次性完成,中间状态可能被其他线程观察到
// 典型的线程安全问题示例
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对象的监视器
        // 临界区代码
    }  // 自动释放监视器
}

执行流程

  1. 进入监视器:线程尝试获取对象的监视器锁
  2. 执行临界区:成功获取锁的线程执行同步代码
  3. 释放监视器:代码执行完毕或异常退出时自动释放锁
  4. 唤醒等待线程:其他等待的线程被唤醒重新竞争

技术特点和优势

synchronized的核心特性

  • 互斥性:确保同一时刻只有一个线程访问
  • 可见性:保证线程间的内存可见性
  • 可重入性:同一线程可以多次获取同一把锁
  • 自动释放:异常或正常退出都会自动释放锁

相比手工同步的优势

  • 语法简洁,易于使用和理解
  • 自动管理锁的获取和释放
  • JVM层面优化,性能不断改善
  • 与Java异常处理机制良好集成

🔧 实现原理与源码分析

底层实现机制

synchronized在字节码层面通过monitorentermonitorexit指令实现:

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);
    }
}

三种锁状态详解

  1. 偏向锁:只有一个线程访问同步代码

    • 在对象头记录线程ID
    • 后续访问直接检查线程ID,无需CAS操作
    • 适合单线程重复进入的场景
  2. 轻量级锁:多个线程交替访问,无激烈竞争

    • 使用CAS操作获取锁
    • 在栈帧中存储锁记录
    • 适合线程交替执行的场景
  3. 重量级锁:多个线程激烈竞争

    • 基于操作系统互斥量实现
    • 线程会被阻塞和唤醒
    • 适合高竞争的并发场景

💡 实战案例与代码示例

具体项目应用

场景:电商系统库存扣减

在电商系统中,库存扣减是典型的并发场景。多个用户同时购买商品时,必须确保库存数据的一致性。

/**
 * 电商库存管理系统
 */
@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的区别?

标准答案

特性synchronizedvolatile
原子性✅ 保证❌ 不保证
可见性✅ 保证✅ 保证
有序性✅ 保证✅ 保证
阻塞性会阻塞不会阻塞
适用场景复合操作状态标志

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) {
    // 安全的同步
}

📚 总结与技术对比

核心要点回顾

  1. synchronized是Java内置的同步机制,基于Monitor模式实现
  2. 锁升级机制提升了性能:偏向锁→轻量级锁→重量级锁
  3. 使用简单但功能有限,适合基础的同步需求
  4. JVM层面优化包括锁消除、锁粗化等技术
  5. 正确使用需要注意锁粒度、死锁预防等问题

与相关技术对比

同步机制性能功能使用复杂度适用场景
synchronized中等基础简单基本同步需求
ReentrantLock丰富中等复杂同步场景
ReadWriteLock专用中等读多写少
StampedLock最高高级复杂高性能读写
Atomic类最高专用简单无锁编程

持续学习建议

  1. 深入JUC包:学习ReentrantLock、Semaphore等高级同步工具
  2. 理解JVM优化:研究HotSpot的锁优化策略
  3. 实践项目应用:在实际项目中积累同步设计经验
  4. 关注新特性:跟踪新版本JDK的并发改进
  5. 学习无锁编程:掌握CAS和原子操作的使用

下一篇预告:《volatile关键字与内存可见性》将深入探讨Java内存模型、happens-before关系和指令重排序的核心机制。

Comments

Link copied to clipboard!