多线程(进阶)面试重点

一、常见的锁策略

锁策略,和普通程序猿基本没啥关系,和"实现锁"的人才有关系的
这里所提到的锁策略,和Java本身没关系,适用于所有和“锁"相关的情况

1.1、悲观锁和乐观锁

悲观锁:预期锁冲突(锁竞争)的概率很高
乐观锁:预期锁冲突(锁竞争)的概率很低

我们来想个问题:到底是悲观锁处理的事情多,还是乐观锁处理的事情多 举个例子:
现在处于疫情期间
A: 假设A保持一个乐观的态度,他说疫情很快就结束了,然后他就什么什么事情都不管,既来之,则安之
B: 假设B持有一个悲观的态度,他说下一波疫情还是很严重,于是他就去超市里面买了一大堆东西(大米,菜,酱油啥的),此时B就在后面做了很多的事情

所以悲观锁做的事情比较多
因此,乐观锁做的事情比较少,消耗掉资源比较少,成本低
悲观锁做的事情比较多,消耗的资源比较多,成本高

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

1.2、读写锁 VS 普通互斥锁

对于普通互斥锁来说,只有两个操作:加锁和解锁
如果多个线程(两个及两个以上)对同一个对象操作就会产生互斥

> 对于读写锁来说,分成了三个操作
1、加读锁:如果代码中只进行读操作,那就加读锁
2、加写锁: 如果代码中只进行写操作,那就加写锁
3、解锁

这个时候,就相当于将 读锁 和 写锁 给分开了。 针对 读锁 和 读锁
之间,是不存在互斥关系的,这是因为多个线程同时读操作不改变内存里面的数据,所以是线程安全的 读锁 和 写锁之间, 写锁 和
写锁之间,才需要互斥

所以,读写锁,我们就自然而然的就将 读锁 和 写锁 分开来写 而且,我们在很多实际场景中都是读操作多,写操作少。
我在讲数据库的时候,期间就提到过这件事。
数据库中的索引就适用于读多写少的情况,因为我们去针对一个有索引的表进行修改,这个操作本身就很低效,但是如果是查询(读操作),那就非常的高效。

读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock /unlock 方法进行加锁解锁.

Synchronized 不是读写锁.

1.3、重量级锁 vs 轻量级锁

CPU 提供了 “原子操作指令”.
操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
JVM 基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类.

这和上面的乐观锁和悲观锁有理解上一定的重叠,相当于乐观锁和悲观锁是原因,而重量级锁和轻量级锁是结果
重量级锁:所做的事情多了,开销也就大了
轻量级锁:所做的事情少了,开销因为就小了

通常情况下:也可以认为,悲观锁就是重量级锁,乐观锁就是轻量级锁
但是!这种说法并不绝对!这种对应关系并不是百分百正确的。
因为 悲观锁 和 乐观锁,表示的是一种“处理锁冲突的态度”。(原因)

而重量级锁和轻量级锁,表示的是“处理锁冲突的结果”。
就是说:我们已经处理好锁冲突,已经把代码实现了。然后,发现这种实现方式有点“重”(开销有点大),或者说发现这种实现方式 “很轻”(开销很小)。

更具体的来说:
在使用的锁中,如果锁是基于内核的一些功能来实现的。【内核态】
比如:调用了操作系统提供的 mutex 接口,此时一般认为这是一个重量级锁。
因为操作系统的锁,会在内核中做很多的事情,开销也就很大,“重量”有点重。

如果锁是用户态去实现的,此时一般认为这是一个轻量级锁。一般认为 用户态的代码要更可控,也更高效。

1.4,自旋锁和挂起等待锁

挂起等待锁:往往就是通过内核的一些机制来实现的,往往较重,[重量级锁的一种典型实现]
自旋锁:往往就是通过用户态代码来实现的,往往教轻 [轻量级锁的一种典型实现]

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.
但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题

自旋锁伪代码:

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

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会
在极短的时间内到来.
一旦锁被其他线程释放, 就能第一时间获取到锁.

理解自旋锁 vs 挂起等待锁
想象一下, 去追求一个女神. 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了~~ 挂起等待锁: 陷入沉沦不能自拔… 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?” (注意, 这个很长的时间间隔里,女神可能已经换了好几个男票了).
自旋锁: 死皮赖脸坚韧不拔. 仍然每天持续的和女神说早安晚安. 一旦女神和上一任分手, 那么就能立刻抓住机会上位.

自旋锁是一种典型的 轻量级锁 的实现方式.

优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是 不消耗 CPU 的)

1.5、公平锁和非公平锁

公平锁:当多个线程等待一把锁的时候,谁先来的,谁就先获取锁(遵循先来后到的规则)
非公平锁:当多个线程等待一把锁的时候,哪一个线程获取到锁的概率是相同的(不遵守先来后到)

对于操作系统来说,本身线程之间的调度就是随机的(机会均等的),操作系统提供的mutex这个锁,就是属于非公平锁
考虑到相同优先级的情况.
实际开发中很少会手动修改线程的优先级(改了之后在宏观上的体会并不明显)

要想实现公平锁反而要付出更多的代价,(得整个队列,来把这些参与竞争的线程给排一排先来后到)

1.6、可重入锁和不可重入锁

一个线程针对同一个对象加两把锁,如果不会死锁,那就是可重入锁,
如果会死锁你,那就是不可重入锁

总结

锁策略不止以上说的这些,还有很多,这只是常见的一部分

1.7、常见的面试题

1) 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁 认为多个线程访问同一个共享变量冲突的概率较大,会在每次访问共享变量之前都去真正加锁.
乐观锁 认为多个线程访问同一个共享变量冲突的概率不大,并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
悲观锁 的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
乐观锁 的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突. (实现细节参考上面的图).

2) 介绍下读写锁?
读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要用在 “频繁读, 不频繁写” 的场景中.

3) 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
自旋锁,如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.

相比于挂起等待锁,
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源

4) synchronized 是可重入锁么?
是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁
的线程就是持有锁的线程, 则直接计数自增

5)synchronized
1.既是一个乐观锁,也是一个悲观锁. (根据锁竞争的激烈程度,自适应)
⒉.不是读写锁,只是一个普通互斥锁.
3.既是一个轻量级锁,也是一个重量级锁(根据锁竞争的激烈程度,自适应)
4.轻量级锁的部分基于自旋锁来实现.重量级的部分基于挂起等待锁来实现.
5.非公平锁.
6.可重入锁.

二、CAS

2.1、什么是CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“,
一个 CAS 涉及到以下操作:
在这里插入图片描述

此处所谓的 CAS ,指的是 CPU 提供了一个单独的 CAS 指令,通过这一条CPU指令,就可以完成上述伪代码要做的所有事情(比较并且交换)。
我们此处讨论的CAS,其实讨论的就是这一条CPU的指令。

另外,这个代码很明显是线程不安全的。
在这里插入图片描述

其实我们代码中很多的功能,都是属于既可以让硬件实现,也可以是软件实现。
就像刚才这段比较交换逻辑,这就相当于硬件直接实现出来了,通过这一条指令,封装好,让我们直接使用。

CAS最大意义
就是让我们写这种多线程安全的代码的时候,提供了一个新的思路和方向!! !
CAS就和锁就不一样了! !
前面讲过的办法就是加锁和解锁,把线程不安全的代码放在锁里,来保证线程安全。这是一种办法。现在我们又看到了另外一种:通过CAS来去进行实现,
关键就在CAS本身操作就只有一条指令,那么它就是原子的,是线程安全的。

2.2、CAS 如何帮我们解决一些线程安全问题?

2.2.1、基于CAS 能够实现“原子类”(这是工作经常用到的)

Java标准库里提供了一组原子类,针对所常用多一些int, long, int array…进行了封装,可以基于CAS的方式进行修改,并且线程安全

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Später321

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值