创建线程

  1. 继承 Thread

image-20260516202545583

  1. 实现 Runnable接口(最常用 ✅)

image-20260516202709450

  1. 实现 Callable+ FutureTask(有返回值)

image-20260516202742673

  1. 使用线程池

线程状态

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就是基于 UnsafeCAS 实现的

AtomicInteger.incrementAndGet就是自旋的CAS操作,直到+1成功

四、 ABA 问题与解决方案

1. 什么是 ABA 问题?

CAS 只检查 V 是否等于 E。但它无法感知在这个过程中,V 是否发生过变化。

场景模拟:

  1. 线程1 读到 A (E=A)。
  2. 线程2 进来,把 A 改成 B,然后又改回了 A。
  3. 线程1 执行 CAS,发现 V 还是 A,跟期望的一样,于是成功更新。

2. 解决:带版本号的 CAS (AtomicStampedReference)

为了解决 ABA,我们不能只比较“值”,还要比较“版本号”。

Java 提供了一个类叫 AtomicStampedReference

  • 它维护了一个 对象引用 和一个 **整数版本戳 (Stamp)**。
  • CAS 的时候,既要比较对象引用(V == E),也要比较版本戳(Stamp == ExpectedStamp)。

synchronized相关

一、 核心概念:它到底锁住了什么?

首先要明确:synchronized实现的同步锁是基于对象的,而不是基于代码块或方法。

  1. 修饰实例方法:锁住的是**当前实例对象 (this)**。

  2. 修饰静态方法:锁住的是**当前类的 Class 对象 (Class<?>)**。因为 Class 对象是全局唯一的,所以相当于锁住了整个类。

  3. 修饰代码块:锁住的是括号里配置的对象synchronized(obj)锁住 obj)。

在Java面试中,synchronized是一个必考的知识点。仅仅回答“它是一个关键字,用于保证线程安全”是远远不够的。面试官通常希望听到你对 JVM 底层实现、对象头结构、锁升级机制以及 JDK 优化(偏向锁/轻量级锁)的理解。

下面我将为你整理一份由浅入深、直击面试痛点synchronized原理详解。


一、 核心概念:它到底锁住了什么?

首先要明确:synchronized实现的同步锁是基于对象的,而不是基于代码块或方法。

  1. 修饰实例方法:锁住的是**当前实例对象 (this)**。

  2. 修饰静态方法:锁住的是**当前类的 Class 对象 (Class<?>)**。因为 Class 对象是全局唯一的,所以相当于锁住了整个类。

  3. 修饰代码块:锁住的是括号里配置的对象synchronized(obj)锁住 obj)


二、 底层实现原理

  1. Monitor (管程/监视器) —— 重量级锁的实现(1.6之前)

    每个 Java 对象天生自带一个 Monitor(监视器) 对象。当线程执行 synchronized代码时,必须先获取到该对象的 Monitor。

    • monitorenter: 执行该指令时,线程尝试获取 Monitor 的所有权。如果获取失败,线程会被阻塞并进入EntryList(等待队列)。
    • monitorexit: 执行该指令时,释放 Monitor,并唤醒 EntryList 中的阻塞线程。
  2. 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队列(双向链表)


链表节点设计

image-20260517174812123


在共享模式下,可能会出现不能唤醒的状况;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
2
3
ReentrantLock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();

核心原理就是在CLH队列(==双端队列==)的基础上+Condition 等待队列(==单向队列==)

1. await() 做了什么?(等待)

当一个线程持有锁,但发现条件不满足(比如队列空了没法取数据),调用 condition.await()

  1. 释放锁:将 AQS 的 state减到 0,完全释放锁(这一步很关键,如果不释放,其他线程没法改条件)。
  2. 入队:将当前线程封装成一个 Node,放入 Condition 自己的等待队列
  3. 挂起:调用 LockSupport.park(),线程进入 WAITING状态,彻底停住。

结果:线程放弃了锁,进入了 Condition 的休息室睡觉。

2. signal() 做了什么?(唤醒)

当另一个线程修改了条件(比如往队列里放了数据),调用 condition.signal()

  1. 转移节点:将 Condition 等待队列中的第一个 Node(线程)取出来。
  2. 重新竞争:将这个 Node 转移到 AQS 的同步队列 尾部。
  3. 等待唤醒:此时这个线程还在 WAITING,直到 AQS 同步队列的前驱节点(持有锁的线程)调用 unlock()时,才会轮到它去抢锁。

ReentrantLock和synchronized比较

image-20260518001709644

其他几个AQS

ReentrantReadWriteLock

原理:使用 AQS 的 state 变量的高 16 位表示读锁,低 16 位表示写锁。读锁是共享模式,写锁是独占模式

应用场景读多写少的场景。例如:本地缓存、配置文件解析。读操作可以并发,写操作必须互斥

使用方式

image-20260518001320472

Semaphore

原理:使用 AQS 的 state 表示“许可证”数量。调用 acquire()时 state 减 1(共享模式),调用 release()时 state 加 1

应用场景限流。控制同时访问特定资源的线程数量。例如:数据库连接池(限制连接数)、停车场(限制车位)

使用方式

image-20260518001345397

CountDownLatch

原理:使用 AQS 的 state 表示倒计时计数器。初始化时设定数值,调用 countDown()时 state 减 1,直到为 0 时唤醒所有等待的线程

应用场景等待其他线程完成任务后再继续。例如:主线程等待所有子任务初始化完成后再启动;或者并行计算,等待所有线程算完再汇总。

使用方式

image-20260518001451162

CyclicBarrier

原理:**基于 ReentrantLock(内部基于 AQS) 和 Condition**。它通过计数器实现,线程调用 await()时计数器减 1,若为 0 则唤醒所有等待线程,并可复用(Cyclic)

应用场景线程间互相等待,都到达某一点后再同时继续。例如:多阶段的计算任务,每一阶段结束后都需要等待其他线程,然后一起进入下一阶段。

使用方式

image-20260518001625735