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

目录

锁策略:

[1. 乐观锁 vs 悲观锁](#1. 乐观锁 vs 悲观锁)

[2. 轻量级锁 vs 重量级锁](#2. 轻量级锁 vs 重量级锁)

[3. 自旋锁 vs 挂起等待锁](#3. 自旋锁 vs 挂起等待锁)

[4. 公平锁 vs 非公平锁](#4. 公平锁 vs 非公平锁)

[5. 可重入锁 vs 不可重入锁](#5. 可重入锁 vs 不可重入锁)

[6. 读写锁 vs 互斥锁](#6. 读写锁 vs 互斥锁)

[Java中 synchronized 内部实现策略 (内部原理)](#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 初始使用乐观锁策略, 当发现锁竞争频繁的时候, 就会自动切换成悲观锁策略

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

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

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

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

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

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

  3. synchronized 不是读写锁

死锁相关

什么死锁

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

死锁的三种典型情况 :

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

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

  3. N个线程, M把锁

如何避免死锁 ?

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

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

死锁的四个必要条件 :

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

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

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

// 也是锁的基本特性

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

// 取决于代码结构

  1. 循环等待 : 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 , 就可以很好的解决死锁问题

锁消除

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

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

锁粗化

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

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

相关推荐
琢磨先生David7 分钟前
重构数字信任基石:Java 24 网络安全特性的全维度革新与未来防御体系构建
java·web安全·密码学
程序修理员30 分钟前
技能点总结
java
Jennifer33K1 小时前
报错_NoSuchMethodException: cn.mvc.entity.User.<init>()
java
爱吃烤鸡翅的酸菜鱼1 小时前
【SpringMVC】概念引入与连接
java·开发语言·mysql
小白学大数据1 小时前
Python自动化解决滑块验证码的最佳实践
开发语言·python·自动化
碎梦归途1 小时前
23种设计模式-行为型模式之策略模式(Java版本)
java·开发语言·jvm·设计模式·策略模式·行为型模式
Albert Edison1 小时前
Python入门基础
开发语言·python
三个蔡1 小时前
Java求职者面试:从Spring Boot到微服务的技术深度探索
java·大数据·spring boot·微服务·kubernetes
小余吃大鱼1 小时前
OpenStack私有云详细介绍
开发语言·php·openstack
画个大饼1 小时前
Swift:什么是Optional?其背后的机制是什么?什么是Unconditional Unwrapping?
开发语言·ios·swift