【JUC第二章下】:锁机制&关键字

🔥你好我是fengxin_rou这是我的个人主页 fengxin_rou的主页

❄️欢迎查看我的专栏我的专栏

《Java后端学习》《JAVASE基础》《JUC并发》《redis》《JVM虚拟机》《MYSQL》《黑马点评》《rabbitmq》《JavaWeb+AI的talis学习系统》《苍穹外卖》

目录

前言:

[synchronized 修饰普通方法、静态方法、代码块,锁的对象分别是谁?](#synchronized 修饰普通方法、静态方法、代码块,锁的对象分别是谁?)

[1. synchronized 修饰 普通方法](#1. synchronized 修饰 普通方法)

[2. synchronized 修饰 静态方法](#2. synchronized 修饰 静态方法)

[3. synchronized 修饰 代码块](#3. synchronized 修饰 代码块)

volatile关键字的作用是什么?

1.保证对所有线程保持可见性

2.保证volatile前后指令不会重排序

为什么volatile不能用来保证原子性?

什么是可重入锁?为什么需要可重入?

什么是公平锁、非公平锁?优缺点?

非公平锁吞吐量为什么比公平锁大?

悲观锁和乐观锁区别、适用场景?

悲观锁适用场景

乐观锁适用场景

[CAS 原理是什么?自旋、Unsafe 类作用?](#CAS 原理是什么?自旋、Unsafe 类作用?)

[CAS 三大问题:ABA、循环耗时、只能保证单个变量原子性,怎么解决?](#CAS 三大问题:ABA、循环耗时、只能保证单个变量原子性,怎么解决?)

什么是自旋锁?优缺点?

什么是死锁,死锁的产生条件是什么?

什么是偏向锁撤销、重偏向?

批量重偏向(高频考点)

轻量级锁自旋次数自适应?

[什么是锁粗化、锁消除?JVM 优化手段?](#什么是锁粗化、锁消除?JVM 优化手段?)

1.锁消除:

2.锁粗化


前言:

前面一篇【JUC第二章上】:锁机制&关键字我们初步学习了JUC锁相关的知识,这一章对起里面的关键字做出一些补充

synchronized 修饰普通方法、静态方法、代码块,锁的对象分别是谁?

一句话总结:普通方法锁实例, 静态方法锁类模板, 代码块锁括号里。

1. synchronized 修饰 普通方法

锁的是 调用这个方法的对象(this)

复制代码
public synchronized void method() { }
  • 锁对象:this
  • 场景:多个线程操作同一个对象时,会互斥
  • 不同对象之间 互不影响

2. synchronized 修饰 静态方法

锁的是 类的 Class 对象

复制代码
public static synchronized void method() { }
  • 锁对象:类名.class
  • 场景:全局锁,整个类只有一把锁
  • 不管多少个对象,全部互斥

3. synchronized 修饰 代码块

锁的是 括号里的对象

复制代码
synchronized(锁对象) { }
  • 锁对象:你自己指定(this、class、自定义对象都可以)
  • 最灵活,推荐使用

注意:普通方法锁(this)静态方法锁(class)两把完全不同的锁, 它们之间 **不会互斥,**可以同时执行!

volatile关键字的作用是什么?

1.保证对所有线程保持可见性

当一个变量被声明为volatile变量时,这个变量就会进入主存,主存是对所有线程可见的,只要一修改该变量,那么其他线程都可以看到

2.保证volatile前后指令不会重排序

volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序

JMM实际定义了4种内存屏障:StoreStore(写写屏障)、StoreLoad屏障(写读屏障)、LoadLoad(读读屏障)、LoadStore(读写屏障)

1)在vloatile之前 插入StoreStore 屏障,可以防止 volatile之前的普通写重排序到volatile写之后

2)在 volatile 写操作之后 插入 StoreLoad 屏障:防止 volatile 写与后面可能出现的 volatile 读/写发生重排序,这是开销最大的屏障。

3)在 volatile 读操作之后 插入 LoadLoad 屏障:防止 volatile 读与后面的普通读发生重排序。

4)在 volatile 读操作之后 插入 LoadStore 屏障:防止 volatile 读与后面的普通写发生重排序。

为什么volatile不能用来保证原子性?

volatile 的核心作用只有两个:

  1. 保证变量的可见性
  2. 禁止指令重排序

但是 volatile 完全不具备 "原子性保障" 的能力。

i++ 这种操作,不是一条指令 ,而是三步操作的组合

  1. 读取主内存的值
  2. 执行 +1 计算
  3. 写回主内存

这三步是可被打断 的。 volatile 没有任何机制能把这三步捆绑成一个不可分割的整体。

所以 volatile 不能保证原子性,不能解决多线程下的并发安全问题。

什么是可重入锁?为什么需要可重入?

可重入锁是指同一个线程可以多次持有锁并且不会造成死锁

复制代码
void a() {
    lock(); // 第一次拿锁 → 计数=1
    b();    // 里面又 lock()
}

void b() {
    lock(); // 同一个线程再次拿锁 → 计数=2
    unlock();
}

这里的计数器是属于锁的,不是方法的。所以在同一个线程里能够持续计数

计数器的规则是拿到锁计数+1,失去锁-1,直到重新回到0该线程才是完全释放锁

为什么需要可重入:为了避免同一个线程调用嵌套加锁方法时,自己把自己锁死,也就是产生死锁

复制代码
例子:
java
运行
void a() {
    lock();
    b(); // 内部又 lock()
}

void b() {
    lock();
}

如果锁不可重入:

线程进入 a () → 获取锁

调用 b () → 再次请求锁

发现锁被自己持有 → 阻塞等待自己释放

自己等自己 → 死锁!

什么是公平锁、非公平锁?优缺点?

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁,即排在第一个的线程才能获取锁
    优点:每一个线程一段时间后都能获取锁,各个线程平等
    缺点:整体执行速度慢,吞吐量小
  • 非公平锁:是指多个线程同时竞争锁,抢到的锁的线程才能执行 ,抢不到的去等待队尾等待
    优点:是整体执行效率高,吞吐量大
    缺点:可能产生线程饥饿问题,如果有线程一直插队,那么一个队列可能很久都不能执行

非公平锁吞吐量为什么比公平锁大?

  • 公平锁::获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
  • 非公平锁:当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率

悲观锁和乐观锁区别、适用场景?

  • 乐观锁: 对于并发间操作产生的线程安全问题持乐观状态 ,乐观锁认为竞争不总是会发生 ,因此它不需要持有锁 ,将比较-替换 这两个动作作为一个原子操作尝试去修改内存中的变量 ,如果失败则表示发生冲突,那么就应该有相应的重试逻辑
  • 悲观锁: 对于并发间操作产生的线程安全问题持悲观 状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,直接上了锁就操作资源了。

乐观锁:不上锁,写入内存失败解决是重试

悲观锁:用上锁的方法防止写入失败

悲观锁适用场景

  • 写操作多
  • 并发竞争激烈
  • 要求数据强一致性
  • 例:库存扣减、交易、金融数据

乐观锁适用场景

  • 读操作多
  • 高并发、冲突少
  • 追求高性能
  • 例:商品详情、缓存更新、计数服务

CAS 原理是什么?自旋、Unsafe 类作用?

CAS 是一种乐观锁机制:内存位置的值(V),预期的值(A)和新值(B),如果内存位置的值V和预期的值A一致,那么久将V替换成B,这一操作是原子性的,通常由硬件指令执行,如在现代处理器上,cmpxchg 指令可以实现 CAS 操作。

自旋:是竞争锁失败后,不进入阻塞,而是进入重试CAS,直到拿到锁

在轻量锁时会触发

作用:避免线程阻塞带来的内核态 / 用户态切换开销,提升性能。

Unsafe类:Unsafe 是 Java 底层的一个 "危险工具类",可以直接操作内存、CAS、线程挂起 / 唤醒。

作用

  1. 实现 CAS 原子操作

    AtomicIntegerReentrantLock 底层全靠它

  2. 直接操作内存

    分配内存、释放内存、修改对象字段

  3. 线程调度

    park()unpark() 挂起 / 唤醒线程(LockSupport 底层)

二者联系:

自旋 = 循环重试

Unsafe = 提供 CAS 原子操作指令

复制代码
while(true) {
    // 自旋循环
    if(Unsafe.compareAndSwap(...)) { // CAS 成功 → 拿到锁
        break;
    }
    // 失败 → 继续自旋
}

CAS 三大问题:ABA、循环耗时、只能保证单个变量原子性,怎么解决?

ABA:在CAS执行过程中,另外一个线程把A改成B之后再改回A,对于执行CAS过程的线程这个过程是无感的。就会导致操作认为未变更:

  • 线程1读取变量为A,准备改为C
  • 此时线程2将变量ABA
  • 线程1的CAS执行时发现仍是A,但状态已丢失中间变化。

解决方法:Java 提供的工具类会在 CAS 操作中增加版本号(Stamp)或标记 ,每次修改都更新版本号,使得即使值相同也能识别变更历史。比如,可以用 AtomicStampedReference 来解决 ABA 问题,通过比对值版本号识别ABA问题。

java 复制代码
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);

// 尝试修改值并更新版本号
boolean success = ref.compareAndSet(100, 200, 0, 1); 
// 前提:当前值=100 且 版本号=0, 才会更新为(200, 1)

循环耗时:是指CAS自旋问题,如果一直重试那么就会一直自旋空转循环,浪费CPU

解决方法:

1.jdk1.6后的自旋自适应:前面有线程自旋成功了,当前自旋时间就久一点,前面自旋失败了,当前就自旋时间短点

2.锁升级机制,如果自旋次数达到阈值,会升级为重量锁陷入阻塞

3.直接适用Lock锁,锁竞争激烈时,失败后直接陷入阻塞

只能保证单个变量原子性:

CAS 只能对一个变量做原子操作。

比如:

复制代码
a++;
b++;

你不能用一个 CAS 同时保证 a 和 b 都原子。

这叫:不能保证多行操作的原子性

解决办法:加sychornized锁,对这一块加锁,让两个操作包起来,能体现整体的原子性

什么是自旋锁?优缺点?

自旋锁:在争抢锁失败之后不进入阻塞状态,而是进入自旋不断重试,直到拿到锁
优点

不会进入阻塞,避免了从用户态到内核态转换的开销

响应速度快,因为没有阻塞

缺点

占用CPU:如果一直没有强锁成功,会一直空转自旋循环,消耗CPU

不适合长耗时的任务:持锁线程执行时间久,自旋线程一直空转,性能急剧下降。

什么是死锁,死锁的产生条件是什么?

死锁:两个线程互相持有对方需要的锁,且不会主动释放锁,互相等待卡死
产生条件:

1.互斥条件

一个锁只能由一个线程持有,其他线程只能等待释放才能拿到

2.不可剥夺条件

锁在持有阶段不会被其他线程抢占,只能自己释放

3.请求与保持条件

在持有一个锁的时候,又去请求另一个锁,且不释放锁

4.循环条件

A持有B的请求锁,请求B里面的锁

B持有A的请求锁,请求A里面的锁

什么是偏向锁撤销、重偏向?

偏向锁是 JVM 为减少无竞争场景下锁开销设计的轻量级锁 ,默认开启,适用于单线程反复获取锁的场景。

触发场景:

有第二个线程尝试获取已偏向的锁 ,偏向锁就会被撤销

执行流程

  1. 锁对象当前偏向线程 A,线程 B 来抢锁;
  2. JVM 进入安全点,暂停持有偏向锁的线程 A;
  3. 检查线程 A 是否还在使用该锁:
    • 情况 1 :A 已退出同步块 → 直接取消偏向 ,锁降级为无锁,B 正常获取;
    • 情况 2 :A 仍在持有锁 → 撤销偏向 ,升级为轻量级锁,两个线程开始自旋竞争。

关键特点

  • 撤销会进入安全点,有性能开销;
  • 偏向锁一旦撤销,默认不会再重新偏向(旧逻辑);
  • 撤销是偏向锁走向轻量级锁 / 重量级锁的第一步。

重偏向(Bias Rebias):

锁对象已经撤销过偏向 ,当原偏向线程彻底不再使用该锁 后,锁可以重新偏向到新线程 ,这个过程就叫重偏向

触发条件(单对象重偏向)

  1. 锁曾经偏向线程 T1,后因竞争被撤销;
  2. T1 长时间不再访问该锁;
  3. 新线程 T2 多次获取这把锁;
  4. JVM 判定当前无多线程竞争 ,将锁重新偏向给 T2

批量重偏向(高频考点)

一个类下大量对象都发生偏向撤销 (阈值默认 20): JVM 认为该类锁存在线程切换但竞争不激烈 ,会把该类所有已撤销偏向的对象,统一允许重偏向,后续新线程获取时直接偏向新线程,减少撤销开销。

行为 偏向锁撤销 重偏向
触发原因 多线程竞争锁 原偏向线程不再使用锁,新线程独占
锁状态变化 偏向 → 无锁 / 轻量级锁 无偏向 → 重新偏向新线程
性能影响 高(进安全点、暂停线程) 低(纯标记修改)
发生时机 锁竞争瞬间 竞争消失、单线程复用锁

轻量级锁自旋次数自适应?

JDK 1.6 之后引入了自适应自旋锁。 自旋次数不再固定,而是由锁的历史自旋成功率决定:

  • 如果该锁上之前自旋成功过,JVM 会让当前线程自旋更久
  • 如果该锁上自旋很少成功,JVM 会减少自旋次数甚至直接跳过自旋 ,快速升级为重量级锁。 目的是减少无用自旋,提升并发性能

什么是锁粗化、锁消除?JVM 优化手段?

锁粗话和锁消除都是JVM种**JIT(即时编译器)**优化代码的手段,目的是让代码的效率更高

1.锁消除:

定义:当一个代码块里不存在线程冲突时,就消除锁

原因:JIT 会做逃逸分析 : 如果一个对象只在当前线程里使用 ,没有逃逸到其他线程,那就不可能产生线程竞争,加锁完全多余。

触发条件:

  • 开启逃逸分析(JDK 7+ 默认开启)
  • 对象不逃逸出当前线程
  • 同步块无竞争可能

**作用:**完全消除无意义的锁开销,性能提升巨大。

2.锁粗化

定义:把连续多次加锁解锁,合并成一次大锁。

原理:如果一段代码里反复对同一个对象加锁、解锁 ,JIT 会把这些锁合并成一个更大范围的锁,减少频繁加锁解锁的消耗。

代码示例

复制代码
// 未优化前:多次加锁解锁
for (int i = 0; i < 1000; i++) {
    synchronized (lock) {
        // 每次循环都加锁 → 释放
    }
}

锁粗化后:

复制代码
// 优化后:只加锁一次
synchronized (lock) {
    for (int i = 0; i < 1000; i++) {
        // ...
    }
}

**作用:**减少频繁加锁解锁带来的性能损耗。

相关推荐
该昵称用户已存在1 小时前
从闭源采购到开源自建:MyEMS MIT 协议下的企业能源中台架构自主权
架构·能源
隔窗听雨眠1 小时前
从过度工程到务实设计:后端架构模式的真实价值
架构
国科安芯2 小时前
AS32S601芯片抗辐照性能试验验证与空间环境适应性分析
前端·分布式·单片机·嵌入式硬件·架构·risc-v·安全性测试
步步为营DotNet2 小时前
深挖.NET 11:.NET Aspire 在云原生应用韧性架构构建的探索与实践
云原生·架构·.net
2601_956743682 小时前
上海物联网开发公司:从设备接入到数据架构的技术拆解
物联网·架构·开发经验·上海
hz567892 小时前
军工视频会议系统技术架构解析:安全隔离环境下高可靠通信的实战方案
安全·架构·音视频·实时音视频·信息与通信
今晚务必早点睡3 小时前
2026 最新互联网架构演进:从“云原生”走向“AI 原生”
人工智能·云原生·架构
深蓝电商API3 小时前
反向海淘系统微服务拆分:从单体到分布式演进实战经验
分布式·微服务·架构·反向海淘
上海云盾第一敬业销售3 小时前
高防CDN架构解析与实战经验
web安全·架构·ddos