多线程进阶 : 八股文面试题 一 [Java EE 多线程 锁和死锁相关问题]

news/2025/2/26 17:40:09

目录

锁策略:

1. 乐观锁 vs 悲观锁

2. 轻量级锁 vs 重量级锁

3. 自旋锁 vs 挂起等待锁

4. 公平锁 vs 非公平锁

5. 可重入锁 vs 不可重入锁

6. 读写锁 vs 互斥锁

Java中 synchronized 内部实现策略 (内部原理)

Java中的synchronized具体采用了哪些锁策略呢?

死锁相关

什么死锁

死锁的三种典型情况 :

如何避免死锁 ? 

死锁的四个必要条件 :

如何解决死锁

锁消除

锁粗化


锁策略:

// 实现一把锁的时候, 针对这个锁要进行的一些设定

1. 乐观锁 vs 悲观锁

// 悲观锁 : 总是假设最坏的情况, 每次去拿数据的时候都认为别人会修改, 所以每次在拿数据的时候都会加锁, 这样别人想拿这个数据就会阻塞, 直到它拿到锁

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

2. 轻量级锁 vs 重量级锁

// 锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的

// CPU 提供了 "原子操作指令"

// 操作系统基于 CPU 的原子指令, 实现 mutex 互斥锁

// JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 reentrantlock 等关键字和类

// 重量级锁 : 加锁机制重度依赖了 OS 提供了mutex 

// 大量的内核态用户态切换 ; 很容易引发线程的调度

// 轻量级锁 : 加锁机制尽可能不使用 mutex , 而是尽量在用户态代码完成, 实在搞不定再使用 mutex

// 少量的内核态用户态切换 ; 不太容易引发线程调度 

3. 自旋锁 vs 挂起等待锁

// 自旋锁 : 当第一次获取锁失败后, 立即再尝试获取锁, 无限循环, 直到获取到锁为止, 这样一旦锁被其他线程释放, 就能第一时间获取到锁

// 自旋锁是一种典型的轻量级锁的实现方式, 其优点为: 没有放弃 CPU , 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁; 缺点是: 如果锁被其他线程持有的时间比较久, 就会持续消耗 CPU 的资源 (挂起等待的时候不消耗 CPU 资源)

// 挂起等待锁 : 当第一次获取锁失败后, 就挂起等待 (阻塞等待), 一直等到系统调用再次调度才能获取锁

4. 公平锁 vs 非公平锁

// 公平锁 : 遵循 "先来后到" , 当有锁释放后按照顺序获取锁

// 非公平锁 : 不遵循 "先来后到" , 当有锁释放后每个需要锁的进程都可以获取锁

// 注意 : 操作系统内部的线程调度就可以视为是随机的, 如果不做任何额外的限制, 锁就是非公平锁, 如果要实现公平锁, 就需要依赖额外的数据结构来记录线程的先后顺序; 公平锁和非公平锁没有好坏之分, 关键看适用场景

// synchronized 是非公平锁

5. 可重入锁 vs 不可重入锁

// 可重入锁 : 允许同一个线程多次获取同一把锁

// 比如在一个递归函数里面有加锁操作, 递归过程中这个锁会阻塞自己吗? 如果不会, 那么这个锁就是可重入锁 (就因为这个原因, 可重入锁又叫做递归锁)

// Java 里只要以 Reentrant 开头命名的锁都是可重入锁, 而且 JDK 提供的所以现成的Lock 实现类, 包括 synchronized 关键字锁都是可重入的, 而 Linux 系统提供的 mutex 是不可重入锁

6. 读写锁 vs 互斥锁

// 读写锁 : 在执行加锁操作时需要额外表明读写意图, 读者之间互不排斥, 而写者之间则要求与任何人互斥

// 一个线程对于数据的访问, 主要存在两种操作: 读数据和写数据

// 两个线程都只读一个数据, 此时并没有线程安全问题, 直接并发读就行

// 两个线程同时写一个数据, 此时就会存在线程安全问题

// 一个线程读另一个线程写, 也会存在线程安全问题

// 读写锁是将 读操作和写操作区分对待, Java 标准库中提供了 ReentrantReadWriteLock 类, 实现了读写锁

// 读写锁特别适用于 "频繁读, 不频繁写" 的场景中

// synchronized 不是读写锁 

Java中 synchronized 内部实现策略 (内部原理)

// 代码中写了一个synchronized 之后, 这里可能会产生一系列的 "自适应过程" , 锁升级(锁膨胀)

// 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

// 偏向锁,不是真的加锁, 而只是做了一个 "标记" . 如果有别的线程来竞争锁了, 才会真的加锁, 如果没有, 那么自始至终都不会真的加锁 (加锁本身有一定开销, 能不加就不加, 有人竞争才加) 

// 偏向锁在没有其他人竞争的时候, 就仅仅是一个简单的标记 (非常轻量). 一旦别的线程尝试进行加锁, 就会立刻把偏向锁升级成真正的加锁状态, 让别人阻塞等待

Java中的synchronized具体采用了哪些锁策略呢?

// 因为synchronized 的自适应特性,所以它包含很多锁策略

1. synchronized 既是悲观锁, 也是乐观锁

// synchronized 初始使用乐观锁策略, 当发现锁竞争频繁的时候, 就会自动切换成悲观锁策略

2. synchronized 既是重量级锁, 也是轻量级锁

3. synchronized 重量级锁部分是基于系统的互斥锁实现的; 轻量级锁部分是基于自旋锁实现的

// 轻量级锁 : synchronized 通过自旋锁的方式来实现轻量级锁

// 我这边把锁占据了, 另一个线程就会按照自旋的方式, 来反复查询当前的锁是否被释放了, 但是, 后续如果竞争这把锁的线程越来越多 (锁竞争更激烈了), 从轻量级锁, 升级成重量级锁

4. synchronized 是非公平锁 (不会遵循先来后到, 锁释放之后, 哪个线程拿到锁, 各凭本事)

5. synchronized 是可重入锁 (内部会记录那个线程拿到了锁, 记录引用计数)

6. synchronized 不是读写锁

死锁相关

什么死锁

死锁是指在多进程或多线程系统中,两个或两个以上的进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的僵局状态,若无外力作用,这些进程(线程)都将无法向前推进

死锁的三种典型情况 :

1. 一个线程, 一把锁, 但是是不可重入锁. 该线程针对这个锁连续加锁两次, 就会出现死锁

2. 两个线程, 两把锁, 这两个线程先分别获取到一把锁, 然后再同时尝试获取对方的锁

3. N个线程, M把锁

如何避免死锁 ? 

// 首先要明确死锁产生的原因, 即 : 死锁的四个必要条件

// 想产生死锁那么四个必要条件缺一不可, 所以只要能够破坏其中的任意一个条件都可以避免出现死锁情况

死锁的四个必要条件 :

1. 互斥使用 : 一个线程获取到一把锁之后, 别的线程不能获取到这个锁

// 实际使用的锁, 一般都是互斥的 (锁的基本特性)

2. 不可抢占 : 锁只能被持有者主动释放, 而不能是被其他线程直接抢走

// 也是锁的基本特性

3. 请求和保持 : 这个一个线程去尝试获取多把锁, 在获取第二把锁的过程中, 会保持对第一把锁的获取状态

// 取决于代码结构

4. 循环等待 : t1 尝试获取 locker2, 需要 t2 执行完, 释放 locker2; t2 尝试获取 locker1, 需要 t1 执行完, 释放 locker1

// 代码展示一下产生死锁时的情况

java">Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2) {
                    System.out.println("t1 两把锁加锁成功!");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker1) {
                    System.out.println("t2 两把锁加锁成功!");
                }
            }
        });
        t1.start();
        t2.start();

// 取决于代码结构, 是日常解决死锁问题的最关键要点

如何解决死锁

1. 经典算法 : 银行家算法

2. 比较简单的一个解决死锁的办法 : 针对锁进行编号, 并且规定加锁的顺序

// 比如 : 约定, 每个线程如果想要获取多把锁, 必须先获取编号小的锁, 后获取编号大的锁 

// 将上面的代码进行更改, 即 : 都先获取锁 locker1 , 就可以很好的解决死锁问题 

锁消除

// 编译器, 会智能的判断, 当前这个代码, 是否必要加锁

// 如果你写了加锁, 但是实际上没有必要加锁, 就会把加锁操作自动删除掉

锁粗化

// 关于"锁的粒度" : 如果加锁操作里面包含的实际要执行的代码越多, 就认为锁的粒度越大

// 具体 "锁的粒度" 要根据实际情况来确定, 没有好坏之分

 


http://www.niftyadmin.cn/n/5868998.html

相关文章

DeepSeek05-大模型WebUI

一、说明: 将DeepSeek部署到前台Web界面的方法主要有以下几种推荐方案,涵盖开源工具、第三方客户端及特定场景适配方案: Open WebUIChatbox AICherry StudioSillyTavern 二、Open WebUI 安装配置教程 特点:Open WebUI 是一个开…

The First项目报告:MyShell开启AI创作经济新纪元

随着加密货币和区块链技术的不断发展,MyShell作为一个前瞻性的Web3 AI平台,迅速崭露头角。MyShell致力于通过去中心化的方式,将AI技术与区块链相结合,为全球创客社区提供一个开放、模块化的AI应用生态。2025年2月25日,…

idea里的插件spring boot helper 如何使用,有哪些强大的功能,该如何去习惯性的运用这些功能

文章精选推荐 1 JetBrains Ai assistant 编程工具让你的工作效率翻倍 2 Extra Icons:JetBrains IDE的图标增强神器 3 IDEA插件推荐-SequenceDiagram,自动生成时序图 4 BashSupport Pro 这个ides插件主要是用来干嘛的 ? 5 IDEA必装的插件&…

【WebDav】坚果云使用WebDav访问文件夹内文档大于750份无法返回问题

坚果云分页多次加载解决办法 问题坚果云使用WebDav访问限制 现象PropFind请求返回数据少于文件夹内数据坚果云请求响应体坚果云请求响应头 结论文档遍历实现python循环方式实现 问题 坚果云使用WebDav访问限制 在批量请求时使用大部份的WebDav库请求坚果云时都会出现仅请求到前…

Fisher信息矩阵与Hessian矩阵:区别与联系全解析

Fisher信息矩阵与Hessian矩阵:区别与联系全解析 在统计学和机器学习中,Fisher信息矩阵(FIM)和Hessian矩阵是两个经常出现的概念,它们都与“二阶信息”有关,常用来描述函数的曲率或参数的敏感性。你可能听说…

AI 编码 2.0 分析、思考与探索实践:从 Cursor Composer 到 AutoDev Sketch

在周末的公司【AI4SE 效能革命与实践:软件研发的未来已来】直播里,我分享了《AI编码工具 2.0 从 Cursor 到 AutoDev Composer》主题演讲,分享了 AI 编码工具 2.0 的核心、我们的思考、以及我们的 AI 编码工具 2.0 探索实践。 在这篇文章中&am…

docker安装etcd:docker离线安装etcd、docker在线安装etcd、etcd镜像下载、etcd配置详解、etcd常用命令、安装常见问题总结

官方网站 官方网址:etcd 二进制包下载:Install | etcd GitHub社区项目:etcd-io GitHub GitHub社区项目版本历史:Releases etcd-io/etcd GitHub 一、镜像下载 1、在线下载 在一台能连外网的linux上执行docker镜像拉取命令…

【PGCCC】PostgreSQL 的弊端(第一部分):临时表

PostgreSQL 是世界上最强大的数据库系统之一。我一直对它的强大功能充满热情,尤其是它的现代 SQL 语言功能。 然而,这并不意味着一切都很好。有些地方还是很麻烦。新手用户如果不知道这个问题,可能会遇到麻烦,而且我发现这种情况…