JavaEE多线程进阶(上)
2026/9/24 4:29:05 网站建设 项目流程

1. 常见锁策略

用锁前需了解锁

1.1 乐观锁vs悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁, 这样别⼈想拿这个数据就会阻塞直到它拿到锁。其他线程想要访问,就阻塞等待,从根源上防止冲突。

如:synchronized、ReentrantLock等。

乐观锁:假设数据⼀般情况下不会产⽣并发冲突,所以在数据进⾏提交更新的时候,才会正式对数据是否产⽣ 并发冲突进⾏检测,如果发现并发冲突了,则让返回⽤⼾错误的信息,让用户决定如何去做。

如:CAS、数据库版本号机制。

1.2 重量级锁vs轻量级锁

重量级锁:依赖操作系统互斥锁(mutex),竞争失败线程直接阻塞。轻量级锁下线程自旋多次抢不到锁就会升级为重量级锁。

轻量级锁:加锁机制尽可能不使⽤mutex,而是尽量在用户态代码完成。实在搞不定了,再使⽤mutex。偏向锁发生竞争,锁还没有释放 → 撤销偏向锁,升级为轻量级锁。

轻量级锁重量级锁
底层机制CAS 自旋,用户态操作系统内核互斥锁
竞争失败行为线程自旋空转线程阻塞挂起
CPU 开销自旋消耗 CPU几乎不耗 CPU
开销来源CPU 空循环线程上下文切换
适用场景竞争少,锁持有时间短竞争激烈,锁持有时间长

通俗比喻:

  • 轻量级锁:原地转圈等。别人拿着锁,你不睡觉,不停尝试抢;如果对方很快用完,你直接拿到;如果对方迟迟不放,你一直转圈很累(耗 CPU)。
  • 重量级锁:坐下睡觉等。抢不到就直接睡觉,放弃 CPU;别人用完锁再叫醒你;睡觉不费力气,但是叫醒、睡觉这个动作本身代价很高(上下文切换)。

背诵短句

  • 轻量级锁:CAS 自旋,不阻塞,消耗 CPU,适合竞争少、锁时间短;
  • 重量级锁:操作系统阻塞线程,有上下文切换开销,适合竞争激烈、锁时间长。

1.3 自旋锁

自旋锁:线程获取锁失败时,不把自己阻塞挂起,循环不停重试抢锁,直到拿到锁。线程不会放弃 CPU,一直在 CPU 上跑循环。

特点:

  • 抢不到锁不阻塞,原地循环 CAS。
  • 没有线程阻塞、没有操作系统上下文切换。
  • 会占用 CPU,一直空转消耗 CPU 资源。

伪代码:

while (抢锁(lock) == 失败) {}

优点:线程不会放弃 CPU,不发生线程阻塞、操作系统调度切换。锁一旦释放,线程可以立刻获取锁,响应快。

缺点:若锁被占用时间较长,线程持续空转,会大量消耗 CPU,而阻塞挂起的线程几乎不消耗 CPU。

1.4 公平锁vs非公平锁

公平锁:线程按照申请锁的先后顺序排队,先到先得,不允许插队。

非公平锁:允许新来的线程直接尝试抢锁,可以插队,不一定按排队顺序获取。

注意:由于操作系统内部的线程调度是随机的,正常情况可以视为非公平锁。

1.5 可重入锁vs不可重入锁

可重入锁:允许一个线程多次获取同一把锁,不会阻塞自己,也叫递归锁,如reentranlock和synchronized。

不可重入锁:简单来说就是把自己锁死,如果一个线程没有释放锁,反而再次申请锁就会锁死自己,操作系统的mutex就是不可重入锁。

1.6 读写锁

(1)读与读之间不会有线程安全问题,直接并发读取。

(2)读与写、写与写之间存在线程安全问题,需要进行互斥。

Java标准库提供ReentrantReadWriteLock类来使用读写锁,ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁,ReentrantReadWriteLock.WriteLock 类表示⼀个写锁,这两个对象都提供了lock和unlock进行加锁解锁。

代码:

package practice.thread; import java.util.concurrent.locks.ReentrantReadWriteLock; class RW{ // 共享数据 public static int data = 0; // 读写锁对象 private static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); // 读锁 private static ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); // 写锁 private static ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); public RW(){ } //读操作 public void read(){ readLock.lock(); try { // 读数据 System.out.println("当前线程"+Thread.currentThread().getName()+"执行读操作,data为:" + data); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } finally { // 需要手动解锁(注意) readLock.unlock(); } } //写操作 public void write(int val){ writeLock.lock(); try { data = val; System.out.println("当前线程"+Thread.currentThread().getName()+"已执行写操作,data为:"+data); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); }finally{ writeLock.unlock(); } } } public class Demo25 { public static void main(String[] args) throws Exception{ RW rw=new RW(); new Thread(()->{ rw.read(); },"读1").start(); new Thread(() -> { rw.read(); }, "读2").start(); new Thread(()->{ rw.write(10); },"写1").start(); new Thread(() -> { rw.write(20); }, "写2").start(); } }

结果:

2. CAS

2.1 CAS是什么

CAS 全称 Compare And Swap,比较并交换,是硬件 CPU 提供的原子指令,是乐观锁的底层实现基础。
CAS 包含 3 个操作数:

  • V:内存中变量实际值
  • A:预期旧值
  • B:准备写入的新值

执行逻辑:

  1. 比较内存真实值V与预期值A是否相等
  2. 如果相等,就把V更新为新值B,返回成功
  3. 如果不相等,说明已经被其他线程修改,直接返回失败,不做修改

伪代码:

boolean CAS(address, expectValue, swapValue){ if(getMemory(address) == expectValue){ setMemory(address,swapValue); return true; } return false; }

注意:比较 + 交换这整套动作,是一条 CPU 原子指令,不会被线程打断。Java 通过Unsafe类封装 CAS 操作。

2.2 CAS 的应用

1. 实现原子类
java.util.concurrent.atomic包下面的原子类(AtomicInteger)底层基于 CAS 实现,不需要加锁保证原子性。

getAndIncrement()自增伪代码:

public int getAndIncrement(){ int oldValue = value; while(!CAS(value,oldValue,oldValue+1)){ oldValue = value; } return oldValue; }

循环执行 CAS,直到修改成功。

2. 实现自旋锁
循环 CAS 尝试抢占锁,抢不到就不断重试。

2.3 CAS 存在的问题 — ABA 问题

1. 什么是 ABA
线程 1 读取变量值为 A,准备执行 CAS 修改;在线程 1 执行 CAS 之前,线程 2 把变量 A 修改为 B,又改回 A。此时内存值依旧是 A,CAS 校验会判定没有发生修改,执行更新;但数据中间已经被篡改过,产生逻辑错误。

举例:账户余额 100,线程 A 要扣款 50;中途线程 B 把余额改成 50,又转账改回 100;此时 A 的 CAS 依旧执行扣款,业务出错。

2. ABA 解决方案
引入版本号(时间戳),每次修改版本号自增;CAS 同时比较值 + 版本号。
Java 提供AtomicStampedReference,内部维护数据 + 版本戳,专门解决 ABA 问题。

2.4 CAS 其他缺点

  1. 竞争激烈时,循环自旋大量消耗 CPU。
  2. 只能保证单个变量的原子操作,不能同时操作多个共享变量。

3. synchronized原理

synchronized 是 JVM 实现的锁,锁只能升级,不能降级。
锁状态流转:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

3.1 synchronized 整体特性

  1. 锁竞争少使用乐观策略,冲突频繁切换悲观锁;
  2. 锁状态逐级升级;轻量级锁使用自适应自旋;
  3. 非公平锁、可重入锁、不是读写锁。

3.2 偏向锁

偏向锁的目的:单线程无竞争场景,尽可能消除 CAS 加锁开销。

  1. 第一个线程获取锁,对象头 Mark Word 记录该线程 ID,标记偏向锁。
  2. 同一个线程再次进入同步块,直接判断线程 ID,不需要 CAS 操作。
  3. 释放锁的时候,不会清除偏向标记。标记存在不等于锁被占用。
  4. 当出现其他线程来竞争锁,立刻撤销偏向锁,升级为轻量级锁。

注意:偏向锁只是对象头的标记,不是线程状态。同一个对象,偏向锁只能记录一个线程 ID。

3.3 轻量级锁

触发时机:偏向锁发生竞争,锁还没有被释放,撤销偏向锁升级轻量级锁。

  1. 底层基于 CAS + 自适应自旋锁,工作在用户态,尽量不调用操作系统互斥锁 mutex。
  2. 竞争锁的线程不阻塞,循环 CAS 自旋尝试抢锁。
  3. JVM 自适应调整自旋次数;如果自旋多次依旧获取不到锁,升级重量级锁。

适用场景:竞争不激烈,锁持有时间很短。优点没有内核态切换;缺点自旋消耗 CPU。

3.4 重量级锁

触发时机:轻量级锁自旋多次抢锁失败,膨胀为重量级锁。

  1. 依赖操作系统内核提供的mutex互斥锁;
  2. 获取锁失败的线程停止自旋,被操作系统挂起,进入阻塞队列;
  3. 锁释放后操作系统唤醒等待的线程,重新竞争锁。

存在用户态↔内核态切换,线程上下文切换开销大;适合锁持有时间长、竞争激烈。

3.5 JVM 锁优化(synchronized 额外优化)

锁消除
JVM 编译器检测到锁对象不会发生多线程竞争,直接把加锁解锁代码消除。
例如单线程环境下StringBuffer的synchronized方法,锁直接消除,减少开销。
锁粗化
如果一段代码频繁多次加锁、释放锁;JVM 会把多次加解锁合并成一次大粒度锁,减少频繁锁操作开销。

锁的粒度

加锁和解锁范围代码越多粒度越粗,返回越细

// t1:粗粒度锁:把整个for循环全部包进synchronized Thread t1 = new Thread(() -> { synchronized (locker) { for (int i = 0; i < 50000; i++) { count++; } } }); // t2:细粒度锁:只把count++这一行包进synchronized,循环在锁外面 Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } });

4. 面试背诵小结

  1. CAS:CPU 硬件原子指令,比较并交换;乐观锁实现;存在 ABA 问题,使用版本号AtomicStampedReference解决。
  2. synchronized 锁升级:无锁→偏向锁(单线程无竞争)→轻量级锁(CAS 自旋)→重量级锁(操作系统阻塞),锁只能升级不能降级。
  3. 自旋锁:抢锁失败循环重试;耗 CPU,无上下文切换;适合锁持有时间短。
  4. 悲观锁上来就上锁;乐观锁不加锁,更新校验冲突。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询