JUC:锁机制/关键字

锁机制和关键字

synchronized底层原理

synchronized的实现原理依赖JVM的Monitor监视器锁和对象头Mark Word

synchronized修饰方法和代码块的方式不同:

  • 方法:在方法的访问加上标志,线程进入方法前,JVM会检查这个标志,有标志就得先拿到监视器锁才能执行
  • 代码块:编译器会在代码块前后插入加锁和解锁字节码指令,而且编译器会自动生成异常处理的解锁字节码指令,保证抛异常也能解锁

重点讲代码块中的执行流程:编译后会在代码块入口插入加锁字节码指令,在正常和异常出口插入解锁字节码指令。线程执行加锁指令时。如果成功则进入临界区执行代码,失败就阻塞等待。

对象头

每个Java对象内存中都有对象头,他会根据锁的状态保存不同信息。无锁存哈希码,偏行锁存线程ID,轻量级锁存指针,重量级锁存指向加锁对象的指针。

synchronized 锁升级流程

锁升级:

  1. 从无锁到偏向锁:当一个线程首次访问加锁指令,会将对象头设置为偏向锁
  2. 从偏向锁到轻量级锁:当另一个线程尝试获取被偏向的锁时,JVM讲取消偏向模式,升级为轻量锁
  3. 从轻量锁到重量级锁:当轻量级锁发生竞争时,及CAS失效,就会升级。此时线程进入阻塞,等待锁释放

偏向锁、轻量级锁、重量级锁各自原理和适用场景?

偏向锁

原理:偏向锁会在对象头的MarkWord中记录:当前持有锁的线程ID

若后续还是该线程访问,不需要CAS,不需要加锁就能执行

适用场景:

单线程反复进入同步代码块

轻量级锁

原理:CAS+自旋

线程会在自己的帧栈中创建Lock Record,通过CAS尝试修改对象头获取锁

CAS成功-获得锁

失败-说明存在竞争;自旋等待

适用场景:

少量线程交替竞争

重量级锁

原理:Monitor+操作系统Mutex

竞争失败就会进入Blocked

适用场景:杠比广发激烈竞争

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

普通方法:this对象;当前调用这个方法的实例对象。每个对象都有自己的独立锁;对象A和对象B互补影响

静态方法:这个类的Class对象,不管new了多少个实例,都公用同一把锁

同步代码块:锁obj对象(括号中的指定对象)

vloatile关键字的作用是什么?

核心作用两个:可见性禁止指令重排

可见性:一个线程改了vloatile,其他线程额能立即看到最新值。

禁止指令重排:编译器和CPU为了性能会重排指令。在多线程环境下会出问题,打乱了逻辑顺序。

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

java 复制代码
private static volatile int count = 0;

public static void main(String[] args) throws InterruptedException {
  Thread[] threads = new Thread[1000];
  for (int i = 0; i < 1000; i++) {
      threads[i] = new Thread(() -> count++);
      threads[i].start();
  }
  for (Thread t : threads) t.join();
  System.out.println(count); // 结果大概率不是 1000
}

原因分析:count++实际由三部分构成(读取;+1;写回)当两个线程同时读到5,都执行+1.结果变成了6.但是预期是7.volatile只保证每次读取的都是最新值,不保证读-改-写这个操作是原子性的。

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

可重入锁:同一个线程能重复获取同一把锁,而不会死锁

直观理解就是以嵌套为例,当线程第一次调用a获取到锁时,第二次调用时发现a已经被锁,只能阻塞等待锁释放。锁永远无法释放。这就是死锁
所以可重入锁的产生背景就是Java方法调用经常嵌套

synchronized 和 ReentrantLock 都是可重入锁。

底层通过:记录当前持有锁线程(owner)和计数器(count)实现

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

两者区分的本质时:多个线程抢锁时,是否按先来后到的顺序

默认时非公平锁,业务性能更高
公平锁需要维护等待队列,每次要检查是不是队首,有额外开销

公平锁:

优点:1.不会出现线程饿死2.更适合资源分配场景

缺点:性能低。额外维护等待队列,线程切换更多

非公平锁:

优点:性能高,减少线程挂起和唤醒

缺点:有线程饿死

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

两种不同的并发控制思想

悲观锁:假设竞争必然发生,先加锁,后操作

优点:数据安全

缺点:性能低,并发能力差
适用场景:

写多读少
实现方式

在MySQL中有两种实现方式。

1.排他锁x锁,用select for update。拿到后独占这行数据,别的事务什么都不能进行

2.共享锁s锁,用select lock in share mode。多个事务可以同时持有同一行s锁,但是拿不到x锁

乐观锁:假设竞争不存在,先操作。等操作完之后检查数据有没有被修改。通常用version字段,读的时候将version读出来,更新时通过where来查看version是否被修改过

优点:性能高

缺点:不安全,会出现ABA问题
实现方式

有两种

1.version字段(版本号):额外维护一个版本号。每次数据更新带上版本号。如果查询的影响行数是0,说明数据被别人改过

2.用CAS比较原值:当预期值和当前值不一样,说明数据被别人改过

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

CAS是:硬件级别的原子操作

实现方式:需要三个值:当前值,预期值,更新值。如果当前值==预期值,将当前值更新为更新值。如果!=,自旋重试,直到成功

自旋

while循环

目的:避免线程阻塞。当线程阻塞会出现用户态与内核态的转换(当线程b拿不到锁时,会请求操作系统将线程B挂起),这一步的开销是极高的

自旋适合:锁占用时间短,因为频繁自旋很耗CPU

Unsafe类

JVM提供的底层工具类

作用:绕过JVM安全机制,直接操作:内存,对象,线程,CAS

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

循环耗时

简单来说就是自旋开销大问题

  1. 限制自旋次数:重试10次失败就阻塞
  2. 锁升级:将轻量级锁升级为重量级锁
  3. LongAdder:一个变量拆成多个cell,不同线程操作不同cell,减少CAS冲突

只能保证单变量原子性

问题原因:CAS只能操作一个值

  1. 加锁
  2. 合并变量:将多个变量封装成一个对象,然后CAS整个对象引用

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

多个线程互相等待对方释放资源,导致有所线程都无法继续执行

死锁产生的四个必要条件

  1. 互斥:资源同一时刻只能被一个线程占用
  2. 持有并等待:献策韩国拿着一个锁的同时,还想要另一个锁
  3. 不可剥夺:线程拿到的锁不能被强行抢走
  4. 循环等待:线程之间形成环形的锁依赖

避免死锁:

  1. 固定加锁顺序
  2. 适用trylock超时机制:ReentrantLock的tryLock尝试获取锁,超时就放弃,避免无限等待。
  3. 缩小锁粒度
  4. 避免锁嵌套
相关推荐
legendary_1635 小时前
Type-C浪潮席卷小家电:SINK芯片如何成为快充高压的“心脏“
c语言·开发语言
Struggle_97555 小时前
算法知识-堆相关知识
java·开发语言·算法
Dxy12393102165 小时前
js中Math.min.apply()详解
开发语言·javascript
摇滚侠5 小时前
Java 零基础全套教程,File 类与 IO 流,笔记 175-176
java·开发语言·笔记
Brookty6 小时前
lntelliJ IDEA使用技巧
java·开发语言·intellij-idea·java入门
公孙秋6 小时前
IDEA MyBatisCodeHelper Pro插件高版本解密
java·ide·intellij-idea
AI@独行侠6 小时前
【超详细】IDEA 2025版-常用设置配置-一篇文章带您解决idea 2025版本常用配置
java·ide·intellij-idea
砍材农夫6 小时前
物联网 基于netty控制报文结构(发布与接收)
java·开发语言·前端·javascript·物联网
‎ദ്ദിᵔ.˛.ᵔ₎6 小时前
C++ 智能指针
开发语言·c++