Skip to content

乐观锁 vs. 悲观锁

这是两种广义的并发控制思想。

  • 悲观锁 (Pessimistic Lock)

    • 理念:总认为数据会被其他线程修改,所以在操作数据前先加锁,阻止其他线程访问。
    • 实现:Java中的synchronized关键字和Lock的实现类(如ReentrantLock)都是悲观锁。
    • 适用场景写操作多的场景。先加锁能保证数据写入的正确性。
    • 调用示例: ```java // synchronized public synchronized void testMethod() { // 操作同步资源 }

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

  • 乐观锁 (Optimistic Lock)

    • 理念:总认为数据不会被其他线程修改,所以不加锁。只在更新数据时,检查在此期间数据是否被其他线程修改过。
    • 实现:通过无锁编程实现,最常用的是CAS (Compare And Swap) 算法。Java的原子类(如AtomicInteger)就是基于CAS实现的。
    • 适用场景读操作多的场景。不加锁的特性可以大幅提升读取性能。
    • 调用示例java private AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.incrementAndGet(); // 原子自增操作

CAS (Compare And Swap)

CAS是一种无锁算法,它包含三个操作数: 1. 内存值 V 2. 预期值 A 3. 新值 B

操作流程:当且仅当内存值V等于预期值A时,才用新值B原子性地更新V的值。否则,不做任何操作并通常进行重试(这个重试过程也叫自旋)。

源码分析 (AtomicInteger.incrementAndGet) 1. 底层调用Unsafe类的getAndAddInt方法。 2. 该方法在一个do-while循环中: * 首先,读取当前内存中的值v (getIntVolatile)。 * 然后,调用compareAndSwapInt尝试将内存值从v更新为v + delta。 * 如果更新失败(意味着在读取v之后,有其他线程修改了内存值),循环继续重试,直到成功为止。

CAS的三个主要问题及解决方案 1. ABA问题:一个值从A变为B又变回A,CAS检查时会误认为没有变化。 * 解决:使用版本号机制。JDK 1.5+ 提供了AtomicStampedReference来解决。 2. 循环时间长开销大:如果自旋长时间不成功,会给CPU带来巨大开销。 3. 只能保证一个共享变量的原子操作:对多个变量操作时,无法保证原子性。 * 解决:将多个变量封装成一个对象,然后使用AtomicReference进行CAS操作。

自旋锁 vs. 适应性自旋锁

  • 背景:线程的阻塞和唤醒需要操作系统切换CPU状态,开销较大。如果同步代码块执行时间很短,这个开销可能得不偿失。

  • 自旋锁 (Spin Lock)

    • 理念:当一个线程请求锁但锁被占用时,该线程不被挂起(不放弃CPU),而是执行一个忙循环(即“自旋”),等待锁的释放。
    • 优点:避免了线程切换的开销。
    • 缺点:占用CPU时间。如果锁被占用时间长,会浪费处理器资源。
    • 实现:其原理也是CAS。AtomicIntegerdo-while循环就是一个自旋操作。
    • 限制:自旋有次数限制(默认10次),超过限制仍未获得锁,线程就会被挂起。
  • 适应性自旋锁 (Adaptive Spin Lock)

    • 理念:自旋的时间(次数)不再是固定的,而是动态调整的。
    • 调整依据:根据前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。
    • 规则
      • 如果上一次自旋成功获取了锁,且持有锁的线程正在运行,虚拟机会认为这次自旋也很可能成功,并允许更长时间的自旋。
      • 如果对于某个锁,自旋很少成功,那么后续可能会省略自旋过程,直接阻塞线程。

锁状态:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

这四种状态是针对synchronized关键字的锁优化机制,锁的状态会随着竞争的激烈程度单向升级(不能降级)

基础概念

  • Java对象头 (Object Header)synchronized的锁信息就存储在这里。
    • Mark Word:存储对象的HashCode、分代年龄和锁标志位。其内容会根据锁状态而改变。
    • Klass Pointer:指向对象所属类的元数据指针。
  • Monitor:每个Java对象都关联一个Monitor。synchronized通过获取对象的Monitor来实现同步。Monitor依赖于底层操作系统的Mutex Lock (互斥锁),这涉及到用户态和内核态的切换,开销大,因此被称为重量级锁

四种锁状态

锁状态 锁标志位 Mark Word 存储内容 解决的问题
无锁 01 对象HashCode、分代年龄、偏向锁标志(0) -
偏向锁 01 偏向的线程ID、偏向时间戳、偏向锁标志(1) 只有一个线程访问同步块,减少不必要的CAS
轻量级锁 00 指向栈中锁记录(Lock Record)的指针 多个线程交替执行同步块,通过自旋避免阻塞
重量级锁 10 指向重量级锁(Monitor)的指针 多线程同时竞争,通过阻塞-唤醒机制处理
  • 无锁 (No Lock)

    • 不锁定资源,所有线程都能访问,但只有一个能修改成功。
    • 通过CAS循环尝试修改共享资源。
  • 偏向锁 (Biased Lock)

    • 目标:在无多线程竞争的情况下,消除同步开销。
    • 流程:当一个线程首次获取锁时,将线程ID记录在Mark Word中。之后该线程进出同步块时,只需检查Mark Word中的线程ID是否是自己,无需CAS操作。
    • 升级:当有其他线程尝试获取该锁时,偏向锁撤销,并升级为轻量级锁。
  • 轻量级锁 (Lightweight Lock)

    • 目标:在存在竞争竞争不激烈(线程交替执行)的情况下,通过自旋提高性能,避免线程阻塞。
    • 加锁流程
      1. 线程在自己的栈帧中创建锁记录(Lock Record)。
      2. 将锁对象的Mark Word拷贝到锁记录中。
      3. 通过CAS尝试将锁对象的Mark Word更新为指向锁记录的指针。
      4. 成功则获取锁;失败则说明存在竞争,开始自旋。
    • 升级:如果自旋超过一定次数,或有新的线程加入竞争,锁会升级为重量级锁。
  • 重量级锁 (Heavyweight Lock)

    • 机制:依赖操作系统的Mutex Lock。
    • 流程:除了持有锁的线程,其他所有等待的线程都会进入阻塞状态,等待被唤醒。性能开销最大。

公平锁 vs. 非公平锁

  • 公平锁 (Fair Lock)

    • 理念:线程按照申请锁的先后顺序获取锁,先到先得,就像排队一样。
    • 优点:所有等待的线程最终都能获取锁,不会“饿死”。
    • 缺点:吞吐量相对较低,因为除了队首的线程,其他线程都会阻塞,线程唤醒开销大。
  • 非公平锁 (Non-fair Lock)

    • 理念:线程尝试获取锁时,不看等待队列,直接尝试抢占。失败后才进入队列等待。
    • 优点:吞吐量更高。因为新来的线程可能正好遇到锁释放,无需进入队列等待和唤醒,减少了开销。
    • 缺点:可能导致等待队列中的线程长时间获取不到锁(“饿死”)。

ReentrantLock中的实现 * ReentrantLock默认是非公平锁,但可以通过构造函数指定为公平锁 (new ReentrantLock(true))。 * 其内部实现FairSyncNonfairSync的唯一区别在于lock()方法。 * 公平锁在尝试获取锁之前,会多一个hasQueuedPredecessors()的判断,检查自己是否是等待队列的第一个节点。如果不是,就乖乖排队。非公平锁则没有这个检查,会直接尝试获取锁。

可重入锁 vs. 非可重入锁

  • 可重入锁 (Reentrant Lock) / 递归锁

    • 理念:同一个线程在持有锁的情况下,可以再次获取该锁而不会被阻塞。
    • 作用:避免在一个线程的嵌套方法调用中发生死锁。
    • 实现:Java中的synchronizedReentrantLock都是可重入锁。它们内部通过一个计数器实现:线程每获取一次锁,计数器加1;每释放一次锁,计数器减1。只有当计数器为0时,锁才被真正释放。
    • 示例java public synchronized void methodA() { // ... methodB(); // 同一个线程可直接调用另一个同步方法 } public synchronized void methodB() { // ... }
  • 非可重入锁 (Non-reentrant Lock)

    • 理念:一个线程获取锁之后,无法再次获取该锁,否则会造成死锁。

独享锁 vs. 共享锁

  • 独享锁 (Exclusive Lock) / 排他锁

    • 理念:该锁一次只能被一个线程持有。
    • 特点:获取独享锁的线程既可以读数据,也可以写数据。一旦加锁,其他任何线程都不能再获取任何类型的锁。
    • 实现synchronizedReentrantLock都是独享锁。
  • 共享锁 (Shared Lock)

    • 理念:该锁可以被多个线程同时持有。
    • 特点:主要用于读操作。一个线程获取共享锁后,其他线程仍然可以获取共享锁,但不能获取独享锁。
    • 实现ReentrantReadWriteLock中的读锁是共享锁,写锁是独享锁。

ReentrantReadWriteLock(读写锁)

它内部包含两把锁:ReadLock(共享锁)和WriteLock(独享锁)。 * 读-读:可以并发,不互斥。 * 读-写 / 写-读 / 写-写:互斥。

实现原理 * 它利用AQS(AbstractQueuedSynchronizer)中的一个int类型state字段(32位)来同时表示读锁和写锁的状态。 * 状态分割:将state字段的高16位用于表示读锁的数量,低16位用于表示写锁的重入次数。 * 加锁规则: * 获取写锁 (tryAcquire):只有在没有其他线程持有读锁或写锁时才能成功。 * 获取读锁 (tryAcquireShared):只有在没有其他线程持有写锁时才能成功。