Skip to content

乐观锁 VS 悲观锁

概念: 对并发操作数据时,看待线程同步的不同角度。

  • 悲观锁:

    • 假设: 并发修改频繁,数据冲突可能性高。
    • 策略: 先加锁,再访问数据,确保数据操作的排他性。
    • Java 实现: synchronized 关键字和 Lock 的实现类 (如 ReentrantLock)。
    • 适用场景: 写操作多的场景,保证数据准确性.
    • 乐观锁:

    • 假设: 并发修改较少,数据冲突可能性低。

    • 策略: 不加锁,直接访问数据,更新时检查数据是否被修改过。若未修改,则更新成功;若已修改,则根据策略处理 (报错或重试)。
    • Java 实现: 无锁编程,常用 CAS (Compare-and-Swap) 算法。Java 原子类 (如 AtomicInteger) 的递增操作基于 CAS 自旋实现.
    • 适用场景: 读操作多的场景,提高读取性能.

调用方式示例:

  • 悲观锁:

    ```java // synchronized public synchronized void testMethod() { // 操作同步资源 }

    // ReentrantLock private ReentrantLock lock = new ReentrantLock(); public void modifyPublicResources() { lock.lock(); try { // 操作同步资源 } finally { lock.unlock(); } } ```

  • 乐观锁:

    java private AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.incrementAndGet(); // CAS 自增操作

总结: 悲观锁显式加锁操作同步资源,乐观锁直接操作同步资源,依赖 CAS 机制保证同步.


自旋锁 VS 适应性自旋锁

前提知识: 线程阻塞/唤醒涉及操作系统 CPU 状态切换,开销较大.

  • 自旋锁:

    • 场景: 同步代码块执行时间短,锁占用时间短。
    • 策略: 请求锁的线程 不放弃 CPU 时间片,而是 自旋等待 (循环检测锁是否释放)。若自旋成功,则避免线程切换开销.
    • 缺点: 占用 CPU 时间。锁占用时间长时,自旋浪费 CPU 资源.
    • 自旋次数限制: 默认 10 次,超过限制仍未获得锁,则挂起线程 (可使用 -XX:PreBlockSpin 修改).
    • 实现原理: CAS 算法AtomicInteger 的自增操作中的 do-while 循环即为自旋操作.
    • 适应性自旋锁:

    • 改进: 自旋次数 动态调整,根据 前一次自旋同一个锁 上的 成功率锁持有者 状态决定.

    • 优势: 更智能的自旋策略,减少无效自旋,提高性能.

总结: 自旋锁适用于锁占用时间短的场景,通过自旋避免线程切换开销。适应性自旋锁更智能,动态调整自旋次数.


无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

锁状态: synchronized 锁升级状态,针对 synchronized 关键字.

  • 无锁: 无锁状态。多个线程竞争资源时,都尝试通过 CAS 修改共享变量,但只有一个线程能成功,失败的线程会重试.

  • 偏向锁:

    • 场景: 单线程访问 同步代码块.
    • 策略: 消除 线程获取锁的 CAS 操作。当线程首次获取锁后,将 偏向模式线程 ID 记录在 对象头 Mark Word 中。后续该线程再次访问同步代码块时,只需检查 Mark Word 是否为偏向模式且线程 ID 是否为当前线程 ID,若是,则直接获得锁,无需 CAS 操作.
    • 偏向锁撤销: 当有 其他线程竞争 偏向锁时,偏向模式结束,升级为轻量级锁.
    • 轻量级锁:

    • 场景: 多线程交替 访问同步代码块 (竞争不激烈).

    • 策略: 使用 CAS + 自旋 尝试获取锁。线程在 栈帧 中创建 Lock Record (锁记录),通过 CAS 将 对象头 Mark Word 替换为指向 Lock Record 的指针。若替换成功,获得锁;若替换失败,自旋等待锁释放.
    • 轻量级锁膨胀: 自旋超过一定次数或自旋线程数超过 CPU 核心数,轻量级锁膨胀为重量级锁.
    • 重量级锁:

    • 场景: 多线程并发高,锁竞争激烈.

    • 策略: 线程获取锁失败后,阻塞 进入 等待队列,等待操作系统 唤醒synchronized 默认升级为重量级锁.
    • 缺点: 线程阻塞/唤醒开销大,性能较低.

总结: 锁状态升级顺序:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁. 偏向锁和轻量级锁旨在减少锁开销,提高性能,重量级锁是最后的保障.


公平锁 VS 非公平锁

概念: 多线程获取锁的顺序策略.

  • 公平锁:

    • 策略: 按申请锁的顺序 获取锁。线程进入 等待队列 排队,队列头部线程 优先获得锁.
    • 优点: 等待线程不会饿死,公平性好.
    • 缺点: 吞吐效率相对较低,队列中其他线程阻塞,CPU 唤醒开销较大.
    • 实现: ReentrantLock 可设置为公平锁 (构造函数传入 true).
    • 非公平锁:

    • 策略: 线程尝试获取锁时,直接竞争。若获取不到,则进入等待队列队尾排队。若此时锁刚好释放,插队线程可能直接获取锁,无需阻塞.

    • 优点: 吞吐效率高,减少唤醒开销,线程有机会不阻塞直接获得锁.
    • 缺点: 等待队列中的线程可能饿死或等待时间过长,公平性差.
    • 实现: synchronizedReentrantLock 默认都是非公平锁.

可重入锁 VS 非可重入锁

概念: 同一个线程 重复 获取 同一个锁 的能力.

  • 可重入锁 (递归锁):

    • 特性: 线程在外层方法获取锁后,进入内层方法 自动 获取 同一把锁,不会因为已持有锁而阻塞.
    • 优点: 避免死锁 (一定程度上).
    • Java 实现: synchronizedReentrantLock 都是可重入锁.
    • 非可重入锁:

    • 特性: 线程重复获取锁时,会被 自己之前持有的锁 阻塞,导致死锁.

代码示例 (可重入锁):

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        doOthers(); // 调用另一个 synchronized 方法
    }

    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}

原理分析:

  • AQS (AbstractQueuedSynchronizer): ReentrantLockNonReentrantLock 都继承自 AQS. AQS 维护一个同步状态 status 计数重入次数 (初始值为 0).
  • 可重入锁: 获取锁时,若 status == 0,则设置 status = 1;若 status != 0 且当前线程已持有锁,则 status++ (重入计数增加). 释放锁时,status--,当 status == 0 时,真正释放锁.
  • 非可重入锁: 获取锁时,直接尝试更新 status,若 status != 0,则获取锁失败,线程阻塞. 释放锁时,直接设置 status = 0.

独享锁 (排他锁) VS 共享锁

概念: 锁对资源的访问模式.

  • 独享锁 (排他锁):

    • 特性: 一次只能被一个线程持有. 线程 T 对数据 A 加排他锁后,其他线程 不能 对 A 加 任何类型的锁.
    • 功能: 获得排他锁的线程既能 数据,又能 修改 数据.
    • Java 实现: synchronized 和 JUC 中 Lock 的实现类 (如 ReentrantLock).
    • 共享锁:

    • 特性: 可被多个线程同时持有. 线程 T 对数据 A 加共享锁后,其他线程 只能 对 A 加 共享锁不能排他锁.

    • 功能: 获得共享锁的线程 只能读 数据,不能修改 数据.
    • Java 实现: ReentrantReadWriteLock读锁 (ReadLock).

ReentrantReadWriteLock 示例:

  • 读写锁分离: ReentrantReadWriteLock 包含 读锁 (ReadLock)写锁 (WriteLock).
  • 读锁: 共享锁,允许多个线程同时读取数据,并发读性能高.
  • 写锁: 独享锁,保证 读-写互斥写-读互斥写-写互斥,保证数据一致性.
  • 实现: 读锁和写锁都基于 Sync (AQS 子类) 实现,但加锁方式不同.