synchronized 详解 (Java 并发编程核心)

好的,我们来深入详解 synchronized,主要聚焦其在 Java 并发编程 中的核心作用、工作原理、使用方式和注意事项。它是 Java 中实现线程同步、保证线程安全最基础且最重要的关键字之一。

synchronized 详解 (Java 并发编程核心)

1. 核心目标:线程安全

  • 问题根源: 多线程环境下,多个线程同时访问和修改共享的可变状态(变量、对象、资源)时,由于线程执行的不确定性(CPU 时间片轮换调度),可能导致数据出现不一致、错误或不可预测的结果。这就是竞态条件
  • 解决方案: synchronized 提供了一种互斥锁机制,确保在同一时刻,最多只有一个线程可以执行被 synchronized 保护的代码段或访问被保护的对象。它强制要求线程在进入同步块/方法前必须先获得锁,执行完毕后再释放锁,其他试图获得同一把锁的线程会被阻塞等待。

2. 作用范围与用法

Synchronized 主要用在两个地方:

  • a) 同步实例方法

    public synchronized void myMethod() {
        // 访问或修改 this 对象实例的状态
    }
    
    • 锁对象: 当前实例对象 (this)
    • 效果: 当线程 A 调用 obj1.myMethod() 时,它会尝试获取 obj1 的锁。如果锁可用,线程 A 获得锁并执行方法体;此时,任何其他线程试图调用 obj1任何一个同步实例方法都会被阻塞(因为它们都需要 obj1 的锁)。但调用 obj2.myMethod()obj1 的非同步方法不受影响(因为它们不竞争 obj1 的锁)。
  • b) 同步静态方法

    public static synchronized void myStaticMethod() {
        // 访问或修改类的静态状态
    }
    
    • 锁对象: 当前类的 Class 对象 (例如 MyClass.class)。
    • 效果: 当线程 A 调用 MyClass.myStaticMethod() 时,它会尝试获取 MyClass.class 对象的锁。此时,任何其他线程试图调用 MyClass任何一个同步静态方法都会被阻塞(因为它们都需要 MyClass.class 的锁)。对实例方法和非静态同步方法无影响。
  • c) 同步代码块

    public void myMethod() {
        // 非同步代码...
    
        synchronized (lockObject) { // lockObject 是任意对象引用
            // 需要同步保护的临界区代码
            // 访问或修改共享状态
        }
    
        // 非同步代码...
    }
    
    • 锁对象: 显式指定的任意对象 (lockObject)。这个对象的选择至关重要,它定义了锁的粒度。
    • 效果: 线程进入 synchronized 块前,必须获得 lockObject 的锁。离开块(无论正常退出还是异常退出)时会自动释放锁。
    • 优势: 提供了更精细的控制粒度,可以只同步真正需要互斥访问的部分代码,而不是整个方法,减少锁竞争,提高并发性能。锁对象的选择也更加灵活。

3. 核心特性与原理

  • a) 内置锁 / Monitor Lock: synchronized 在 Java 中是基于 内置锁 (Intrinsic Lock)监视器锁 (Monitor Lock) 实现的。每个 Java 对象都有一个与之关联的内置锁(也称为 monitor)。
  • b) 互斥性 (Mutual Exclusion): 这是 synchronized 最核心的特性,确保同一时间只有一个线程持有特定对象的锁。
  • c) 可重入性 (Reentrancy): 这是关键特性。一个线程可以再次获得它已经持有的锁。 这防止了线程自身造成死锁。
    public class ReentrantExample {
        public synchronized void methodA() {
            methodB(); // 同一个线程可以再次获取 this 锁
        }
        public synchronized void methodB() {
            // ...
        }
    }
    
    • 内部维护一个计数器和一个持有线程标识。第一次获取锁时计数器置为1,同一线程每次重入计数器加1,退出同步块时计数器减1。当计数器归零时,锁才真正被释放。
  • d) 内存可见性 (Memory Visibility): synchronized 不仅提供互斥,还保证了内存可见性。这是通过 Java 内存模型 (JMM) 的 Happens-Before 规则实现的:
    • 解锁 Happens-Before 于后续的加锁: 一个线程在释放锁之前对共享变量所做的修改,对于之后成功获得同一把锁的另一个线程是立即可见的。这确保了在同步块内读取到的数据是最新值,修改后的值也能被其他进入同步块的线程看到。
    • 这解决了 CPU 缓存、编译器指令重排序等导致的内存不一致性问题。

4. 底层实现 (JVM 层面)

  • 字节码层面:
    • 对于同步方法:JVM 通过方法访问标志 ACC_SYNCHRONIZED 来标识。
    • 对于同步代码块:使用 monitorentermonitorexit 两条字节码指令包裹临界区代码。monitorenter 尝试获取锁,monitorexit 释放锁。编译器会确保即使发生异常,monitorexit 也会被执行(通过异常表机制)。
  • Monitor 对象: 每个对象关联一个 Monitor。Monitor 主要包含:
    • Owner: 指向持有锁的线程。
    • EntryList: 存放等待获取锁的线程(竞争锁时失败进入阻塞状态)。
    • WaitSet: 存放调用了 wait() 方法而主动放弃锁并等待唤醒的线程。
  • 锁优化: 现代 JVM(如 HotSpot)对 synchronized 进行了大量优化,使其在常见场景下性能不再像早期版本那样笨重:
    • 偏向锁 (Biased Locking): 假设锁通常由同一个线程获得。首次获取锁时记录线程 ID,后续该线程进入/退出同步块几乎无额外开销。当出现另一个线程竞争时,撤销偏向锁。
    • 轻量级锁 (Lightweight Locking): 当锁处于无竞争或轻微竞争时,通过 CAS (Compare-And-Swap) 操作在用户态尝试获取锁,避免直接进入操作系统内核的阻塞状态。如果 CAS 失败,说明有竞争,升级为重量级锁。
    • 重量级锁 (Heavyweight Locking / Mutex): 真正的操作系统级别的互斥量 (Mutex)。涉及用户态到内核态的切换,线程阻塞和唤醒成本高。这是锁竞争激烈时的最终状态。
    • 锁粗化 (Lock Coarsening): 将临近的多个连续的 synchronized 块合并为一个更大的块,减少锁的获取/释放次数。
    • 锁消除 (Lock Elimination): JIT 编译器通过逃逸分析,证明某个锁对象不可能被其他线程访问到(即对象是线程局部的),则直接移除该锁操作。

5. 使用注意事项与潜在问题

  • a) 性能开销: 虽然优化了很多,但锁操作(尤其是重量级锁)仍有开销。获取/释放锁需要操作系统的介入(系统调用),线程阻塞和唤醒代价高昂。过度或不必要的同步会严重降低并发性能。
  • b) 死锁 (Deadlock): 多个线程相互等待对方持有的锁,导致所有线程永久阻塞。经典死锁条件(互斥、请求与保持、不可剥夺、循环等待)在 synchronized 使用不当时很容易发生。
    // 经典死锁示例
    Thread 1: synchronized(A) { synchronized(B) { ... } }
    Thread 2: synchronized(B) { synchronized(A) { ... } }
    
    • 预防策略: 固定锁的获取顺序、使用超时机制(synchronized 本身不支持,需用 ReentrantLock.tryLock(timeout))、避免嵌套锁。
  • c) 锁粒度过粗/过细:
    • 过粗: 使用一个锁保护大量无关资源(如用 this 锁整个对象),导致并发度低,性能差。
    • 过细: 使用大量不同的锁,虽然提高了并发度,但增加了编程复杂性,更容易出错(如死锁)。需要根据共享资源的关联性合理选择锁对象。
  • d) 活跃性问题:
    • 活锁 (Livelock): 线程不断响应对方动作而无法继续执行(如两个线程都礼貌地让对方先走)。
    • 饥饿 (Starvation): 某些线程长时间无法获得所需资源(如 CPU 时间片或锁),导致无法执行。
  • e) 不适用于所有场景: 对于复杂的同步需求(如尝试获取锁、可中断的锁获取、公平锁、等待通知的多个条件队列),synchronized 提供的功能相对基础。这时 java.util.concurrent.locks 包中的 ReentrantLockCondition 提供了更强大灵活的控制。

6. 最佳实践

  1. 最小化临界区: 只同步真正访问共享资源的代码,尽快释放锁。
  2. 谨慎选择锁对象:
    • 保护实例状态 -> 使用 this 或专门的私有实例锁对象 (private final Object lock = new Object();)。
    • 保护静态状态 -> 使用类的 Class 对象或专门的私有静态锁对象 (private static final Object STATIC_LOCK = new Object();)。
    • 避免使用 String 字面量或基本类型包装类(如 Integer)作为锁对象! 因为它们可能被 JVM 缓存/复用,导致意外的锁共享。
  3. 避免嵌套锁: 如果必须嵌套,严格遵循全局一致的锁获取顺序
  4. 优先使用同步代码块: 通常比同步方法提供更细粒度的控制。
  5. 考虑替代方案: 对于高并发场景或复杂需求,优先考虑 java.util.concurrent 包中的高级并发工具:
    • ReentrantLock (可中断、超时、公平锁、多个条件变量)
    • ReadWriteLock (读写分离)
    • ConcurrentHashMap, CopyOnWriteArrayList 等线程安全容器
    • AtomicInteger, AtomicReference 等原子变量类 (CAS 无锁)
    • CountDownLatch, CyclicBarrier, Semaphore, Exchanger 等同步器
  6. 清晰注释: 对使用 synchronized 的地方进行清晰注释,说明锁保护的是什么资源。

总结

synchronized 是 Java 语言内置的、用于实现线程同步和保证共享数据一致性的关键机制。它通过对象的内置锁提供互斥性和内存可见性,并具有可重入性。理解其工作原理(Monitor、锁优化)、作用范围(方法、代码块、锁对象)、核心特性以及潜在问题(性能、死锁、粒度)对于编写正确且高效的多线程 Java 程序至关重要。在简单场景下,synchronized 是便捷的选择,但在复杂或高性能要求的并发场景中,应积极考虑 java.util.concurrent 包提供的更高级工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值