01CASAQSLOCK

隐式锁(synchronized)

java中的锁包含了内部锁和显示锁。内部锁是通过synchronized关键字实现的 ;显示锁是通过Lock接口来进行实现。

synchronized概述

特点:

1、synchronized关键字可以用来修饰方法(静态和非静态)和代码块。

2、被synchronized修饰的方法被称之为同步方法,被synchronized修饰的代码块被称之同步代码块。

3、同步方法整个方法体都是临界区。同步代码块中所包裹的代码是临界区。

同步代码块的格式

1
2
3
4
5
6
synchronized (对象) {

// 在此代码块中访问共享数据
}

该对象可以是任意的对象,这个对象可以简单的理解就是一把锁:但是需要保证多个线程在访问的时候使用的是同一个对象(但是这个对象本质上不是一个锁,专业的术语将其称之为"监视器"(摄像头))

synchronized原理

synchronized同步代码块的情况

1
2
3
4
5
6
7
8
public class SynchronizedDemo {

public void method() {
synchronized (this) {
System.out.println("synchronized");
}
}
}

通过javap查看字节码文件信息,如下所示:

1
javap -c SynchronizedDemo.class

Java虚拟机包含了一个很实用的命令javap,该命令可以反编译已经编译过的Java Class文件,输出该类文件的详细信息。-c,该选项可以输出类中的所有方法以及字节码信息

其中: monitor就是监视器的含义

image-20251112220823274

在字节码层面由 monitorenter / monitorexit 指令实现。Javac 会生成一个保护区域(try/finally 风格)的结构:正常路径在结束时 monitorexit,异常路径通过异常处理器也会执行 monitorexit,保证锁总是释放,随后重新抛出异常。当执行 monitorenter指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。

sync到底锁什么?四种锁?

在 JDK 1.6 以前,所有的锁都是”重量级“锁,因为使用的是操作系统的互斥锁,当一个线程持有锁时,其他试图进入synchronized块的线程将被阻塞,直到锁被释放。涉及到了线程上下文切换和用户态与内核态的切换,因此效率较低。

这也是为什么很多开发者会认为 synchronized 性能很差的原因。

那为了减少获得锁和释放锁带来的性能消耗,JDK 1.6 引入了“偏向锁”和“轻量级锁” 的概念,对 synchronized 做了一次重大的升级,升级后的 synchronized 性能可以说上了一个新台阶。

在 JDK 1.6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:

  1. 无锁状态
  2. 偏向锁状态(15关闭,18废弃)性能收益不明显;JVM 内部代码维护成本太高
  3. 轻量级锁状态
  4. 重量级锁状态
优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗 CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗 CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行时间较长。

锁什么

对象的“锁”是存放在每个 Java 对象都有一个对象头。

偏向锁

一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID。当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID。

如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费 CAS 操作来加锁和解锁;如果不是,就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用 CAS 来替换 Mark Word 里面的线程 ID 为新线程的 ID,这个时候要分两种情况:

  • 成功,表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁;

  • 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。

    撤销偏向锁

    偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

    偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下:

    1. 在一个安全点(在这个时间点上没有字节码正在执行)停止拥有锁的线程。
    2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和 Mark Word,使其变成无锁状态。
    3. 唤醒被停止的线程,将当前锁升级成轻量级锁。

轻量级锁

JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为 Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。

然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

自旋:不断尝试去获取锁,一般用循环来实现。

JDK 采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

自旋也不是一直进行下去的,如果自旋到一定程度(和 JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁

在释放锁时,当前线程会使用 CAS 操作将 Displaced Mark Word 的内容复制回锁的 Mark Word 里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么 CAS 操作会失败,此时会释放锁并唤醒被阻塞的线程。

重量级锁

重量级锁依赖于操作系统的互斥锁(mutex,用于保证任何给定时间内,只有一个线程可以执行某一段特定的代码段) 实现,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。

锁的升级流程

具体的锁升级的过程是:无锁->偏向锁->轻量级锁->重量级锁

  • 无锁:这是没有开启偏向锁的时候的状态,在JDK1.6之后偏向锁的默认开启的,但是有一个偏向延迟,需要在JVM启动之后的多少秒之后才能开启,这个可以通过JVM参数进行设置,同时是否开启偏向锁也可以通过JVM参数设置。
  • 偏向锁:这个是在偏向锁开启之后的锁的状态,如果还没有一个线程拿到这个锁的话,这个状态叫做匿名偏向,当一个线程拿到偏向锁的时候,下次想要竞争锁只需要拿线程ID跟MarkWord当中存储的线程ID进行比较,如果线程ID相同则直接获取锁(相当于锁偏向于这个线程),不需要进行CAS操作和将线程挂起的操作。
  • 轻量级锁:在这个状态下线程主要是通过CAS操作实现的。将对象的MarkWord存储到线程的虚拟机栈上,然后通过CAS将对象的MarkWord的内容设置为指向Displaced Mark Word的指针,如果设置成功则获取锁。在线程出临界区的时候,也需要使用CAS,如果使用CAS替换成功则同步成功,如果失败表示有其他线程在获取锁,那么就需要在释放锁之后将被挂起的线程唤醒。
  • 重量级锁:当有两个以上的线程获取锁的时候轻量级锁就会升级为重量级锁,因为CAS如果没有成功的话始终都在自旋,进行while循环操作,这是非常消耗CPU的,但是在升级为重量级锁之后,线程会被操作系统调度然后挂起,这可以节约CPU资源。

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

深入理解Java锁升级(图解+史上最全):无锁 → 偏向锁 → 轻量级锁 → 重量级锁-CSDN博客

话术

实现原理

在JVM中,synchronized是基于进入和退出Monitor对象来实现的,无论是显式同步还是隐式同步。对于方法级的同步,它是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作中。JVM可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志来区分一个方法是否为同步方法。当方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有Monitor,然后执行方法,最后在方法完成时释放Monitor。

对于代码块的同步,它是利用monitorentermonitorexit这两个字节码指令实现的。这些指令分别位于同步代码块的开始和结束位置。当JVM执行到monitorenter指令时,当前线程会尝试获取Monitor对象的所有权,如果获取成功,就会执行同步代码块,然后通过monitorexit指令释放Monitor对象

ReentrantLock 和 synchronized 区别

(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。

(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。

(4)synchronized:Java中的关键字,是由JVM来维护的。是JVM层面的锁。

Lock:是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁。

synchronized是底层是通过monitorenter进行加锁(底层是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。只有在同步块或者是同步方法中才可以调用wait/notify等方法的。因为只有在同步块或者是同步方法中,JVM才会调用monitory对象的);通过monitorexit来退出锁的。

而lock是通过调用对应的API方法来获取锁和释放锁的。

(5)synchronized;非公平锁

lock:两者都可以的。默认是非公平锁。在其构造方法的时候可以传入Boolean值。

true:公平锁

false:非公平锁

JVM对Synchornized的优化?

synchronized 核心优化方案主要包含以下 4 个:

  • 锁膨胀:synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。
  • 锁消除:指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。
  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
  • 自适应自旋锁:指通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。

CAS

乐观锁和悲观锁

悲观锁

对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

乐观锁

乐观锁,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行的安全性。

由于乐观锁假想操作中没有锁的存在,因此不太可能出现死锁的情况,换句话说,乐观锁天生免疫死锁

  • 乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;
  • 悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

什么是 CAS

过程

  • V:要更新的变量(var)
  • E:预期值(expected)
  • N:新值(new)

比较并交换的过程如下:

判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,说明已经有其它线程更新了 V,于是当前线程放弃更新,什么都不做。

话术

CAS是解决多线程并发安全问题的一种乐观锁算法。 因为它在对共享变量更新之前,会先比较当前值是否与更新前的值一致,如果一致则更新,如果不一致则循环执行(称为自旋锁),直到当前值与更新前的值一致为止,才执行更新。

Unsafe类是CAS的核心类,提供硬件级别的原子操作(目前所有CPU基本都支持硬件级别的CAS操作)。

缺点

开销大:在并发量比较高的情况下,如果反复尝试更新某个变量,却又一直更新不成功,会给CPU带来较大的压力(自旋)

ABA问题:当变量从A修改为B再修改回A时,变量值等于期望值A,但是无法判断是否修改,CAS操作在ABA修改后依然成功。(ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。从 JDK 1.5 开始,JDK 的 atomic 包里提供了一个类AtomicStampedReference类来解决 ABA 问题。)

不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块(多个共享变量)的原子性。

AQS

1. AQS 的作用是什么?

AQS 是一个用于构建锁和同步器的框架,它通过 FIFO 双向队列 管理等待线程,并通过 volatile int state 变量实现资源状态的原子操作。开发者只需重写少量方法,即可自定义同步工具。

2. AQS 的底层结构

(1) 资源状态state

  • 操作方式
    • getState():获取当前状态值。
    • setState(int newState):直接设置状态值。
    • compareAndSetState(int expect, int update):通过 CAS 原子更新状态。
  • 实际应用
    • ReentrantLockstate 表示锁的重入次数(0=未锁定,1=已锁定,>1=重入)。
    • Semaphorestate 表示剩余的许可证数量。

(2) 等待队列(CLH 变种)

  • 双向链表实现,每个节点(Node)封装一个等待线程。
  • 节点状态:
    • CANCELLED(线程已取消等待)
    • SIGNAL(后继节点需要被唤醒)
    • CONDITION(线程在条件队列中等待)

话术

  1. 经典面试题
1
2
3
4
5
6
7
8
9
AQS 是什么?
AQS 的核心原理是什么?
AQS 中的同步队列和条件队列有什么区别?
ReentrantLock 的公平锁和非公平锁有什么区别?
Semaphore 是如何基于 AQS 实现的?
CountDownLatch 的实现原理与 AQS 有什么关系?
AQS 中独占模式和共享模式的区别是什么?
AQS 中的状态变量 state 有什么作用?
如何自定义一个基于 AQS 的同步器?
  1. 解题思路和答案
    AQS 是什么?

解题思路:从 AQS 的定义、作用和在 Java 并发包中的地位进行阐述。
答案:AQS 即抽象队列同步器(AbstractQueuedSynchronizer) ,是 Java 并发包中构建锁和同步器的基础框架 。它提供了一种基于 FIFO 队列的机制来管理线程的竞争和等待状态 ,AQS 就像是 Java 并发编程大厦的基石。

  1. AQS 的核心原理是什么?
    解题思路:围绕 AQS 的核心构成(状态变量state和同步队列),以及在独占模式和共享模式下线程获取和释放资源的过程进行分析。
    答案:AQS 的核心原理基于一个 FIFO 等待队列和一个同步状态(state) 。state是一个volatile修饰的整型变量,用于表示同步状态 ,不同的同步器中state有着不同的含义 。同步队列是一个双向链表,当线程无法获取同步状态时,会被封装成一个节点加入到同步队列中等待 。在独占模式下,同一时刻只有一个线程能获取到同步状态,其他线程获取失败则进入同步队列等待;在共享模式下,允许多个线程同时获取同步状态,只要剩余的共享资源满足线程的需求 。线程通过CAS操作来竞争获取或释放资源,获取失败则进入同步队列等待唤醒 。
  2. AQS 中的同步队列和条件队列有什么区别?
    解题思路:分别介绍同步队列和条件队列的结构、作用以及节点状态的含义,对比它们在功能和使用场景上的差异。

答案:

同步队列是 AQS 实现线程同步的关键数据结构,是一个双向链表 ,用于存储等待获取同步状态的线程 。当线程获取同步状态失败时,会被封装成一个节点加入到同步队列的尾部 。节点的状态有CANCELLED(线程已取消等待)、SIGNAL(后继节点的线程需要被唤醒)等 。在独占模式下,持有锁的线程释放锁时,会唤醒同步队列中头节点的后继节点对应的线程 ;在共享模式下,释放共享资源时,会唤醒多个等待的线程 。

条件队列与Condition接口相关联 ,每个Condition对象都有一个单独的条件队列 。线程调用Condition的await方法时,会从同步队列转移到条件队列并释放锁进入等待状态 ,此时节点的状态会被设置为CONDITION 。当其他线程调用Condition的signal方法时,条件队列中第一个节点被转移回同步队列,等待获取锁 。

总的来说,同步队列主要用于线程获取同步状态失败时的等待和唤醒,而条件队列用于线程在满足特定条件时的等待和唤醒 ,两者相互配合,共同实现了 AQS 强大的同步功能 。

  1. ReentrantLock 的公平锁和非公平锁有什么区别?
    解题思路:从获取锁的过程、是否保证线程获取锁的顺序以及性能等方面进行对比分析。
    答案:公平锁保证线程按照请求锁的顺序获取锁,即先到先得 。在获取锁时,公平锁会先调用hasQueuedPredecessors方法检查同步队列中是否有前驱节点 ,只有在没有前驱节点时才尝试获取锁 ,以此保证先到的线程先获取锁 。
    非公平锁则不保证获取顺序,线程可以在锁可用时直接竞争获取,可能会出现后到的线程先获取到锁的情况 。非公平锁在获取锁时,直接尝试通过CAS操作获取锁,不会检查同步队列 ,如果获取失败,才会和公平锁一样,将线程加入同步队列等待 。

在性能方面,非公平锁由于减少了检查同步队列的开销,在高并发场景下性能通常比公平锁更高 。但同时,由于可能导致一些线程长时间等待,出现线程饥饿的问题 。而公平锁虽然保证了公平性,但由于频繁的线程上下文切换,性能相对较低 。

  1. Semaphore 是如何基于 AQS 实现的?
    解题思路:分析Semaphore中与 AQS 相关的核心方法,如tryAcquireShared和tryReleaseShared,阐述它们如何利用 AQS 的同步队列和状态变量来实现信号量的功能。
    答案:Semaphore是基于 AQS 的共享模式实现的 。在Semaphore中,AQS 的状态变量state表示剩余的许可数 。当一个线程调用acquire方法获取许可时,实际上调用的是 AQS 的acquireShared方法 ,tryAcquireShared方法是获取共享资源的核心逻辑 。它会进入一个无限循环,获取当前的state,计算获取acquires个许可后剩余的许可数remaining 。如果remaining小于 0,说明当前剩余许可数不足,返回remaining,表示获取失败 ;否则,通过CAS操作尝试将state从当前值更新为remaining 。如果更新成功,返回remaining,表示获取成功 ;如果更新失败,说明有其他线程同时在修改state,继续循环尝试 。
    当线程调用release方法释放许可时,实际上调用的是 AQS 的releaseShared方法 ,tryReleaseShared方法会进入一个无限循环,获取当前的state ,计算释放releases个许可后的状态next 。如果next小于当前state,说明释放的许可数超过了最大许可数,抛出Error异常 ;否则,通过CAS操作尝试将state从当前值更新为next 。如果更新成功,返回true,表示释放成功 ;如果更新失败,说明有其他线程同时在修改state,继续循环尝试 。如果释放成功,会调用doReleaseShared方法唤醒同步队列中等待的线程 。

  2. CountDownLatch 的实现原理与 AQS 有什么关系?
    解题思路:说明CountDownLatch如何利用 AQS 的同步队列和状态变量来实现线程的等待和计数功能,重点分析await和countDown方法的实现逻辑。
    答案:CountDownLatch也是基于 AQS 实现的 。它利用 AQS 的状态变量state来表示计数的初始值 。当一个或多个线程调用await方法时,实际上调用的是 AQS 的acquireSharedInterruptibly方法 ,线程会尝试获取共享资源 。由于CountDownLatch的设计,只有当state减为 0 时,线程才能获取到共享资源,否则线程会被加入到同步队列中等待 。
    当其他线程调用countDown方法时,实际上调用的是 AQS 的releaseShared方法 ,会将state减 1 。如果state减为 0,说明所有的计数都已完成,会唤醒同步队列中等待的线程 ,让它们继续执行 。

  3. AQS 中独占模式和共享模式的区别是什么?
    解题思路:从获取资源的方式、同步队列的处理以及应用场景等方面进行详细对比。

答案:在获取资源的方式上,独占模式下,同一时刻只有一个线程能获取到同步状态,其他线程只能等待 ;共享模式下,允许多个线程同时获取同步状态,只要剩余的共享资源满足线程的需求 。
在同步队列的处理上,独占模式中,获取锁失败的线程会被加入同步队列,当持有锁的线程释放锁时,只会唤醒同步队列中头节点的后继节点对应的线程 ;共享模式中,获取共享资源失败的线程同样会被加入同步队列,当有线程释放共享资源时,会唤醒同步队列中多个等待的线程(只要剩余的共享资源足够) 。

在应用场景方面,独占模式适用于需要严格控制资源访问,保证同一时刻只有一个线程访问资源的场景,如文件写入操作 ;共享模式适用于读多写少的场景,或者需要多个线程同时访问共享资源的场景,如数据库连接池、信号量控制并发访问数量等 。

  1. AQS 中的状态变量 state 有什么作用?
    解题思路:结合不同的同步器,阐述state在表示同步状态、控制资源访问以及实现锁的重入等方面的作用。
1
答案:AQS 中的状态变量state是实现同步的关键 ,它用一个volatile修饰的整型变量来表示同步状态 。在不同的同步器中,state有着不同的含义和作用 。在ReentrantLock中,state表示锁的重入次数 。当一个线程首次获取到锁时,state会被设置为 1 。如果该线程再次获取同一把锁,state就会递增,变为 2,以此类推,表示线程对锁的重入 。而当线程释放锁时,state会相应递减,直到state0 时,锁被完全释放 。在Semaphore中,state表示剩余的许可数 。假设我们创建一个信号量对象并设置许可数为 5,那么初始时state就是 5 。当一个线程调用acquire方法获取许可时,如果state大于 0,该线程就能获取到许可,同时state1 ;当state0 时,其他线程再调用acquire方法就会被阻塞,直到有其他线程调用release方法释放许可,state增加后,才有可能获取到许可 。
  1. 如何自定义一个基于 AQS 的同步器?
    解题思路:介绍自定义同步器的步骤,包括继承 AQS 类、重写必要的抽象方法(如tryAcquire、tryRelease等),以及如何使用自定义同步器。

    1
    答案:自定义一个基于 AQS 的同步器,首先需要继承AbstractQueuedSynchronizer类 。然后,根据同步器的类型(独占模式或共享模式),重写相应的抽象方法 。

11.为什么 AQS 使用双向队列而非单向队列?

1
2
3
4
5
6
7
线程取消的高效处理:当线程因超时或中断取消等待时,需要快速修改前驱和后继节点的指针。

时间复杂度优化:

单向队列删除中间节点需遍历,时间复杂度 O(n)。

双向队列通过 prev 和 next 指针直接操作,时间复杂度 O(1)。

12.AQS 的 ConditionObject 和 Object.wait() 有什么区别?

对比维度 ConditionObject Object.wait()
绑定对象 必须绑定到 AQS 实现的锁(如 ReentrantLock) 任意 synchronized 对象
多条件队列 支持多个条件队列(如生产者-消费者模型分组) 仅一个等待队列
唤醒精度 signal() 精确唤醒指定条件队列的线程 notify() 随机唤醒一个线程
中断支持 提供 awaitUninterruptibly() 方法 需手动处理中断

13AQS 如何处理线程中断?

  • 不可中断模式(默认):*

    1
    线程调用 acquire() 时被中断,会继续等待直到获取资源,再补上中断标记。
  • 可中断模式:*

    1
    线程调用 acquireInterruptibly() 时被中断,直接抛出 InterruptedException。

14共享模式下的唤醒传播是什么?

1
2
3
4
共享模式(如 Semaphore)释放资源时,会唤醒队列中的第一个等待节点,
并由该节点继续唤醒后续共享节点,形成“连锁反应”,直到无可用资源或队列为空。

优势:减少唤醒次数,提升并发效率。

15AQS 中的同步状态(state)有什么作用?

1
2
答:同步状态(state)用于表示同步器的状态,不同的同步器对 state 的含义和操作方式不同。
例如在 ReentrantLock 中表示锁的重入次数,在 Semaphore 中表示可用的许可证数量。

16AQS 队列的作用是什么?

1
答:AQS 队列用于存储等待获取同步状态的线程节点,当线程获取同步状态失败时,会被加入到队列中等待唤醒。

17说说 AQS 中 acquire 和 acquireInterruptibly 方法的区别?

1
2
3
答:acquire 方法是独占式获取同步状态,如果获取失败则将当前线程加入等待队列并阻塞,且不响应中断;
acquireInterruptibly也是独占式获取同步状态,但允许线程被中断,
当线程被中断时会抛出 InterruptedException。

img

显式锁(Lock)

相比同步锁,JUC包中的Lock锁的功能更加强大,它提供了各种各样的锁(公平锁,非公平锁,共享锁,独占锁……),所以使用起来很灵活。

锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。

ReentrantLock

ReentrantLock 重入锁,是实现Lock 接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源重复加锁,即当前线程获取该锁后再次获取不会被阻塞

要想支持重入性,就要解决两个问题:

  1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
  2. 由于锁会被获取 n 次,那么只有锁在被释放同样的 n 次之后,该锁才算是完全释放成功。

Reen公平锁和非公平锁源码区别

公平锁代码的逻辑与 nonfairTryAcquire 基本上一致,唯一的不同在于增加了 hasQueuedPredecessors 的逻辑判断,从方法名就可以知道该方法用来判断当前节点在同步队列中是否有前驱节点的,如果有前驱节点,说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点,才有做后面逻辑判断的必要性。

读写锁

ReentrantReadWriteLock 是 Java 的一种读写锁,它允许多个读线程同时访问,但只允许一个写线程访问,或者阻塞所有的读写线程。这种锁的设计可以提高性能,特别是在数据结构中,读操作的数量远远超过写操作的情况下。

读写锁的实现主要是通过重写 AQS 的 tryAcquire 方法和 tryRelease 方法实现的,读锁和写锁的获取和释放都是通过这两个方法实现的。

读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级。、、

读写降级

读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级,关于锁降级,

这里的流程可以解释如下:

  • 获取读锁:首先尝试获取读锁来检查某个缓存是否有效。
  • 检查缓存:如果缓存无效,则需要释放读锁,因为在获取写锁之前必须释放读锁。
  • 获取写锁:获取写锁以便更新缓存。此时,可能还需要重新检查缓存状态,因为在释放读锁和获取写锁之间可能有其他线程修改了状态。
  • 更新缓存:如果确认缓存无效,更新缓存并将其标记为有效。
  • 写锁降级为读锁:在释放写锁之前,获取读锁,从而实现写锁到读锁的降级。这样,在释放写锁后,其他线程可以并发读取,但不能写入。
  • 使用数据:现在可以安全地使用缓存数据了。
  • 释放读锁:完成操作后释放读锁。

StampedLock

StampedLock 面试中问的比较少,不是很重要,简单了解即可。

[StampedLock 是什么?

StampedLock 是 JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 Condition

不同于一般的 Lock 类,StampedLock 并不是直接实现 LockReadWriteLock接口,而是基于 CLH 锁 独立实现的(AQS 也是基于这玩意)。

1
2
public class StampedLock implements java.io.Serializable {
}

StampedLock 提供了三种模式的读写控制模式:读锁、写锁和乐观读。

  • 写锁:独占锁,一把锁只能被一个线程获得。当一个线程获取写锁后,其他请求读锁和写锁的线程必须等待。类似于 ReentrantReadWriteLock 的写锁,不过这里的写锁是不可重入的。
  • 读锁 (悲观读):共享锁,没有线程获取写锁的情况下,多个线程可以同时持有读锁。如果己经有线程持有写锁,则其他线程请求获取该读锁会被阻塞。类似于 ReentrantReadWriteLock 的读锁,不过这里的读锁是不可重入的。
  • 乐观读:允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。

另外,StampedLock 还支持这三种锁在一定条件下进行相互转换 。

1
2
3
long tryConvertToWriteLock(long stamp){}
long tryConvertToReadLock(long stamp){}
long tryConvertToOptimisticRead(long stamp){}

StampedLock 在获取锁的时候会返回一个 long 型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳,这也是StampedLock不可重入的原因。

[StampedLock 的性能为什么更好?

相比于传统读写锁多出来的乐观读是StampedLockReadWriteLock 性能更好的关键原因。StampedLock 的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了线程饥饿的问题,吞吐量大大提高。

[StampedLock 适合什么场景?

ReentrantReadWriteLock 一样,StampedLock 同样适合读多写少的业务场景,可以作为 ReentrantReadWriteLock的替代品,性能更好。

不过,需要注意的是StampedLock不可重入,不支持条件变量 Condition,对中断操作支持也不友好(使用不当容易导致 CPU 飙升)。如果你需要用到 ReentrantLock 的一些高级性能,就不太建议使用 StampedLock 了。

另外,StampedLock 性能虽好,但使用起来相对比较麻烦,一旦使用不当,就会出现生产问题。强烈建议你在使用StampedLock 之前,看看

StampedLock 的底层原理了解吗?

StampedLock 不是直接实现 LockReadWriteLock接口,而是基于 CLH 锁 实现的(AQS 也是基于这玩意),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。StampedLock 通过 CLH 队列进行线程的管理,通过同步状态值 state 来表示锁的状态和类型。

end?


01CASAQSLOCK
http://example.com/2025/11/13/01CAS,AQS,lock/
作者
無鎏雲
发布于
2025年11月13日
许可协议