Java锁
创建线程
- 继承
Thread类

- 实现
Runnable接口(最常用 ✅)

- 实现
Callable+FutureTask(有返回值)

- 使用线程池
线程状态
1. NEW(新建状态)
- 定义:线程对象已被创建(
new Thread()),但尚未调用start()方法。
2. RUNNABLE(可运行状态)
定义:线程正在 Java 虚拟机中执行。但这个状态涵盖了两个层面:Ready(就绪) 和 Running(运行中)。
- Ready:线程已准备好运行,等待 CPU 调度。
- Running:线程正在 CPU 上执行。
==Thread.yield()暂时释放CPU使用,运行变为就绪,但可能马上获取到CPU又变为运行,但总的来说都是Java的可运行状态==
3. BLOCKED(阻塞状态)
- 定义:线程被阻塞,正在等待获取一个监视器锁(Monitor Lock)。通常发生在试图进入一个被
synchronized修饰的代码块或方法时,且该锁已被其他线程持有。 - 进入此状态的操作:
- 线程尝试进入一个
synchronized同步块/方法,但发现锁已经被其他线程占用。 - 线程在
BLOCKED状态下无法自动恢复,必须等到持有锁的线程释放锁,然后该线程竞争成功后才会退出此状态。
- 线程尝试进入一个
- ==注意:这是唯一一种因锁竞争导致的等待,且特指
synchronized关键字相关的锁。==
4. WAITING(无限期等待状态)
- 定义:线程处于等待状态,无限期地等待另一个线程执行特定操作(通知/中断)。
- 进入此状态的操作:
- 调用
Object.wait()方法(且未设置超时时间)。 - 调用
Thread.join()方法(且未设置超时时间,等待目标线程执行完毕)。 - 调用
LockSupport.park()方法。
- 调用
- 退出条件:依赖其他线程调用
Object.notify()/notifyAll()或LockSupport.unpark(thread)。
5. TIMED_WAITING(限期等待状态)
- 定义:线程处于等待状态,但在指定的时间后会自动唤醒(或者被提前中断/唤醒)。
- 进入此状态的操作:
- 调用
Thread.sleep(long millis)(最典型,不释放锁)。 - 调用
Object.wait(long timeout)。 - 调用
Thread.join(long millis)。 - 调用
LockSupport.parkNanos(...)或LockSupport.parkUntil(...)。
- 调用
- 特点:带有时间参数的等待方法。
6. TERMINATED(终止状态)
- 定义:线程已经执行完毕(run 方法正常结束)或者因异常意外终止。
- 进入此状态的操作:
run()方法执行完成。- 线程抛出一个未捕获的异常(Uncaught Exception)导致意外终止
乐观锁
一、 核心思想:版本号机制 (Versioning)
“数据 + 版本号”,这是乐观锁最典型的应用场景(比如数据库更新:UPDATE table SET value=new_val, version=version+1 WHERE id=1 AND version=old_version)。
查询的时候一个版本号,提交的时候查看当前的版本号和之前查询的版本号做一个对比
二、 CAS (Compare And Swap)
CAS 就是上面“版本号机制”在 CPU 硬件层面和 Java 代码层面的具体实现手段。
1. 什么是 CAS?
CAS 是一条 CPU 原子指令(Atomic Instruction)。
它的功能是:比较并交换。
2. VEN 详解
三个核心参数:
- V (Value / Current Value):内存值。也就是当前主内存中那个共享变量的实际值。
- E (Expected Value):期望值。也就是我这个线程旧读到的值(或者叫旧版本号)。
- N (New Value):新值。我想把 V 更新成这个值。
三、 Unsafe 类与 AtomicInteger
在 Java 中,我们不能直接调用 CPU 指令,而是通过 sun.misc.Unsafe这个类来实现的。
1. Unsafe 类
比如AtomicInteger就是Unsafe包下的一个类
2. AtomicInteger 的底层原理
AtomicInteger就是基于 Unsafe和 CAS 实现的
AtomicInteger.incrementAndGet就是自旋的CAS操作,直到+1成功
四、 ABA 问题与解决方案
1. 什么是 ABA 问题?
CAS 只检查 V 是否等于 E。但它无法感知在这个过程中,V 是否发生过变化。
场景模拟:
- 线程1 读到 A (E=A)。
- 线程2 进来,把 A 改成 B,然后又改回了 A。
- 线程1 执行 CAS,发现 V 还是 A,跟期望的一样,于是成功更新。
2. 解决:带版本号的 CAS (AtomicStampedReference)
为了解决 ABA,我们不能只比较“值”,还要比较“版本号”。
Java 提供了一个类叫 AtomicStampedReference。
- 它维护了一个 对象引用 和一个 **整数版本戳 (Stamp)**。
- CAS 的时候,既要比较对象引用(V == E),也要比较版本戳(Stamp == ExpectedStamp)。
synchronized相关
一、 核心概念:它到底锁住了什么?
首先要明确:synchronized实现的同步锁是基于对象的,而不是基于代码块或方法。
修饰实例方法:锁住的是**当前实例对象 (
this)**。修饰静态方法:锁住的是**当前类的 Class 对象 (
Class<?>)**。因为 Class 对象是全局唯一的,所以相当于锁住了整个类。修饰代码块:锁住的是括号里配置的对象(
synchronized(obj)锁住 obj)。
在Java面试中,synchronized是一个必考的知识点。仅仅回答“它是一个关键字,用于保证线程安全”是远远不够的。面试官通常希望听到你对 JVM 底层实现、对象头结构、锁升级机制以及 JDK 优化(偏向锁/轻量级锁)的理解。
下面我将为你整理一份由浅入深、直击面试痛点的 synchronized原理详解。
一、 核心概念:它到底锁住了什么?
首先要明确:synchronized实现的同步锁是基于对象的,而不是基于代码块或方法。
修饰实例方法:锁住的是**当前实例对象 (
this)**。修饰静态方法:锁住的是**当前类的 Class 对象 (
Class<?>)**。因为 Class 对象是全局唯一的,所以相当于锁住了整个类。修饰代码块:锁住的是括号里配置的对象(
synchronized(obj)锁住 obj)
二、 底层实现原理
Monitor (管程/监视器) —— 重量级锁的实现(1.6之前)
每个 Java 对象天生自带一个 Monitor(监视器) 对象。当线程执行
synchronized代码时,必须先获取到该对象的 Monitor。- monitorenter: 执行该指令时,线程尝试获取 Monitor 的所有权。如果获取失败,线程会被阻塞并进入EntryList(等待队列)。
- monitorexit: 执行该指令时,释放 Monitor,并唤醒 EntryList 中的阻塞线程。
JDK 1.6 之后的优化:锁升级机制
在 JVM 中,任何一个对象在内存中的布局都分为三部分:对象头、实例数据、对齐填充
“
synchronized的锁信息其实是存储在 Java 对象的对象头(Mark Word)里的。JVM 通过检查 Mark Word 中的锁标志位来判断当前对象处于哪种锁状态。”为了降低性能消耗,JDK 1.6 引入了“偏向锁”和“轻量级锁”。现在的
synchronized是一个智能锁,它会根据竞争情况自动升级。锁升级的过程是不可逆的(只能升不能降)
1. 无锁 (No Lock)
对象刚创建时的状态。
2. 偏向锁 (Biased Locking)==单线程==
- 场景:只有一个线程访问同步块。
- 原理:当一个线程第一次访问时,JVM 会将对象头中的“偏向锁位”设为 1,并记录该线程的 ID(Thread ID)。以后这个线程再次进入时,不需要进行 CAS 操作,只需要简单检查一下 Thread ID 是否匹配即可。
- 优点:几乎没有额外开销,像没加锁一样快。
- 撤销:一旦出现第二个线程尝试获取锁,偏向锁就会立即撤销(需要等到全局安全点),升级为轻量级锁。
3. 轻量级锁 (Lightweight Locking)==低竞争==
- 场景:多线程交替执行,没有发生实际的竞争(你用完我用,我没等你)。
- 原理:线程在执行前,会在自己的栈帧中创建一个 Lock Record(锁记录),然后将对象头中的 Mark Word 拷贝过来(Displaced Mark Word)。接着通过 CAS 操作尝试将对象头的指针指向栈中的锁记录。
- 如果成功,获得锁。
- 如果失败(说明有竞争),线程会自旋(Spin)一会儿,不断重试 CAS。
- 优点:避免了用户态到内核态的切换,通过自旋消耗 CPU 来换取性能。
4. 重量级锁 (Heavyweight Locking)==高竞争==
- 场景:多个线程同时竞争锁,自旋失败(自旋次数超过阈值,或者第三个线程来凑热闹)。
- 原理:锁膨胀(Inflate)为重量级锁。Mark Word 指向 Monitor 对象。未抢到锁的线程会被挂起,进入等待队列,此时涉及操作系统层面的线程阻塞和唤醒。
ReentrantLock相关
一、 核心概念:什么是 AQS?
AQS (AbstractQueuedSynchronizer) 是一个用于构建锁和同步器的框架
设计模式:模板方法模式。它定义了一套获取锁和释放锁的骨架流程,具体的逻辑(比如是否允许重入、是否公平)交给子类去实现
核心思想:state(状态) + CLH队列(双向链表)
链表节点设计

在共享模式下,可能会出现不能唤醒的状况;PROPAGATE 状态就是为了防止这种情况,它作为一个‘强制标记’,告诉被唤醒的线程:‘不管你有没有拿到资源,都必须去尝试唤醒下一个,保证唤醒信号能传递下去。’”
两种模式:
- 独占模式
ReentrantLock(独占),state通常表示==重入次数==
- 共享模式
Semaphore/CountDownLatch(共享),state`通常表示==剩余的许可证数量==
二、ReentrantLock
1. State(同步状态)
AQS 内部有一个核心变量 private volatile int state
- 在
ReentrantLock中,state 代表重入次数。state = 0:锁空闲。state > 0:锁被占用,数值表示重入的次数。
2. CLH 队列(等待队列)
当线程抢锁失败时,AQS 会将这个线程封装成一个 Node 节点,放入一个双向链表(虚拟的 CLH 队列)中挂起等待。
3. 公平和非公平
它们的区别仅在于 tryAcquire(尝试获取锁) 这个方法的实现细节不同:
非公平:线程调用 lock()时,先直接 CAS 抢一次,如果 state==0,直接 CAS,抢不到再进队列
公平:线程调用 lock()时,如果 state==0,会先调用 hasQueuedPredecessors()检查队列,队列不空,就不要CAS抢了,加到队尾;如果队列空了才 CAS继续
为什么非公平锁快?
因为减少了线程上下文切换的开销。当一个线程释放锁时,刚好一个新线程进来直接抢到了,就省去了从队列里唤醒线程、重建栈帧的开销。
3.释放资源
release()执行完成后,唤醒头结点的下一个节点
4.Condition (await/signal) 的原理
1 | ReentrantLock lock = new ReentrantLock(); |
核心原理就是在CLH队列(==双端队列==)的基础上+Condition 等待队列(==单向队列==):
1. await() 做了什么?(等待)
当一个线程持有锁,但发现条件不满足(比如队列空了没法取数据),调用 condition.await():
- 释放锁:将 AQS 的
state减到 0,完全释放锁(这一步很关键,如果不释放,其他线程没法改条件)。 - 入队:将当前线程封装成一个 Node,放入 Condition 自己的等待队列。
- 挂起:调用
LockSupport.park(),线程进入WAITING状态,彻底停住。
结果:线程放弃了锁,进入了 Condition 的休息室睡觉。
2. signal() 做了什么?(唤醒)
当另一个线程修改了条件(比如往队列里放了数据),调用 condition.signal():
- 转移节点:将 Condition 等待队列中的第一个 Node(线程)取出来。
- 重新竞争:将这个 Node 转移到 AQS 的同步队列 尾部。
- 等待唤醒:此时这个线程还在
WAITING,直到 AQS 同步队列的前驱节点(持有锁的线程)调用unlock()时,才会轮到它去抢锁。
ReentrantLock和synchronized比较

其他几个AQS
ReentrantReadWriteLock
原理:使用 AQS 的 state 变量的高 16 位表示读锁,低 16 位表示写锁。读锁是共享模式,写锁是独占模式
应用场景:读多写少的场景。例如:本地缓存、配置文件解析。读操作可以并发,写操作必须互斥
使用方式:

Semaphore
原理:使用 AQS 的 state 表示“许可证”数量。调用 acquire()时 state 减 1(共享模式),调用 release()时 state 加 1
应用场景:限流。控制同时访问特定资源的线程数量。例如:数据库连接池(限制连接数)、停车场(限制车位)
使用方式:

CountDownLatch
原理:使用 AQS 的 state 表示倒计时计数器。初始化时设定数值,调用
countDown()时 state 减 1,直到为 0 时唤醒所有等待的线程
应用场景:等待其他线程完成任务后再继续。例如:主线程等待所有子任务初始化完成后再启动;或者并行计算,等待所有线程算完再汇总。
使用方式:

CyclicBarrier
原理:**基于
ReentrantLock(内部基于 AQS) 和Condition**。它通过计数器实现,线程调用await()时计数器减 1,若为 0 则唤醒所有等待线程,并可复用(Cyclic)
应用场景:线程间互相等待,都到达某一点后再同时继续。例如:多阶段的计算任务,每一阶段结束后都需要等待其他线程,然后一起进入下一阶段。
使用方式:

