锁策略
一,悲观锁 VS 乐观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会碰上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁。
乐观锁:假设数据一般不会发生修改,等到别人真的要修改了,就返回错误信息给用户,让用户决定如何去做,如果别人没有修改,那其实就相当于没有上锁,效率大大提高(其实这和懒汉模式有点相似的思路)。
二,重量级锁 VS轻量级锁
我们先了解一下其他概念:
锁的核⼼特性 “原⼦性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的,如下图:
- CPU 提供了 “原子操作指令”.
- 操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁.
- JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
重量级锁:
加锁机制重度依赖了 操作系统 提供的 mutex - 大量的内核态用户态切换
- 很容易引发线程的调度
这两个操作, 成本比较高. ⼀旦涉及到用户态和内核态的切换, 就意味着 “沧海桑田”.
轻量级锁:
加锁机制尽可能不使用 mutex, ⽽是尽量在用户态代码完成. 实在搞不定了, 再使用mutex. - 少量的内核态用户态切换.
- 不太容易引发线程调度.
我们举个例子:
假如:某天我们去银行办理业务,工作人员给了我们两个选择:
1,直接将所有的材料交给他,让他帮我们办理
2,我们在窗口外就将该打印该填写的表都弄完,然后再交给他
想一想,我们应该选哪一个呢?可能大多数人都会选第一个,图省事。但是从效率的角度来讲第二个更加的高效,为什么呢?要知道去办理业务的可不止我们,银行的工作人员拿到的不一定只有我们的一份材料,所以他可能先将所有的材料都先整理在一块,然后统一给办理。这样对于我们来说是不是没有我们将所有的东西弄好直接办理快呢。
三,自旋锁 VS 挂起等待锁
自旋锁:
按之前的⽅式,线程在抢锁失败后进⼊阻塞状态,放弃 CPU,需要过很久才能再次被调度.但实际上, ⼤部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使⽤⾃旋锁来处理这样的问题
伪代码:
1 while (抢锁(lock) == 失败) {}
如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试会在极短的时间内到来.⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
挂起等待锁:
这个锁其实和自旋锁是有点相反的,该锁在获取锁失败后会直接进入阻塞状态,放弃CPU,需要很久才能被调度,这也就是之前我们最常用到的锁。
为了更好理解,举个例子:
想象⼀下, 去追求⼀个⼥神. 当男⽣向⼥神表⽩后, ⼥神说: 你是个好⼈, 但是我有男朋友了~~
挂起等待锁: 陷⼊沉沦不能⾃拔… 过了很久很久之后, 突然⼥神发来消息, “咱俩要不试试?” (注意, 这个很⻓的时间间隔⾥, ⼥神可能已经换了好⼏个男票了).
⾃旋锁: 死⽪赖脸坚韧不拔. 仍然每天持续的和⼥神说早安晚安. ⼀旦⼥神和上⼀任分⼿, 那么就能⽴刻抓住机会上位.
四,公平锁VS非公平锁
假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后 C也尝试获取锁, C 也获取失败, 也阻塞等待.
当线程 A 释放锁的时候, 会发⽣啥呢?
公平锁: 遵守 “先来后到”. B ⽐ C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.
举个例子
这就好⽐⼀群男⽣追同⼀个⼥神. 当⼥神和前任分⼿之后, 先来追⼥神的男⽣上位, 这就是公平锁; 如果是⼥神不按先后顺序挑⼀个⾃⼰看的顺眼的, 就是⾮公平锁.
非公平锁:
其实上述的所谓公平和非公平并不是有什么特殊的含义,其实有些含义可能与我们日常的想法还相反。这都是乌龟的屁股————“ 规定 ”。前人定的,我们照着用就行了。
注意:
- 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是⾮公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
- 公平锁和⾮公平锁没有好坏之分, 关键还是看适⽤场景
- synchronized是非公平锁。
五,可重入锁 VS 不可重入锁
- 可重入锁的字面意思就是“ 可以重新进入的锁 ” 即:允许同一线程多次获取同一把锁。
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁*(出于这个原因,也叫做递归锁)。 - Java里面只要是以Reetrant 开头命名的锁都是可重入锁,而且JDK 提供的所有现成的Lock 实现类,包括synchronized 关键字都是可重入的
- 但是Linux 系统提供的mutex 是不可重入锁。
- 理解:“把自己锁死”:一个线程没有释放锁,然后又尝试再次加锁。
- 举个例子:
- synchronized就是不可重入锁。
六,读写锁
- 多线程之间,数据的读取方之间不会产⽣线程安全问题,但数据的写⼊方互相之间以及和读者之间都需要进⾏互斥。如果两种场景下都⽤同⼀个锁,就会产⽣极⼤的性能损耗(这是由于,我们的读操作本来就不需要加锁,而如果平白一个锁,就是多此一举,很降低效率)。所以读写锁因此⽽产⽣。
- 复读者之间并不互斥,⽽写者则要求与任何⼈互斥。
线程安全问题: - 两个线程都只是读⼀个数据, 此时并没有线程安全问题. 直接并发的读取即可.
- 两个线程都要写⼀个数据, 有线程安全问题.
- ⼀个线程读另外⼀个线程写, 也有线程安全问题
为了解决上诉的线程安全问题,. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁 - ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁. 这个对象提供了 lock / unlock ⽅法
进⾏加锁解锁. - ReentrantReadWriteLock.WriteLock 类表⽰⼀个写锁. 这个对象也提供了 lock / unlock⽅法进⾏加锁解锁.
注意 - 读加锁和读加锁之间, 不互斥.
- 写加锁和写加锁之间, 互斥.
- 读加锁和写加锁之间, 互斥.
- 只要是涉及到 “互斥”, 就会产⽣线程的挂起等待. ⼀旦线程挂起, 再次被唤醒就不知道隔了多久了.因此尽可能减少 “互斥” 的机会, 就是提⾼效率的重要途径.
- 读写锁特别适合于 “频繁读, 不频繁写” 的场景中. (这样的场景其实也是⾮常⼴泛存在的,如: 利用教务系统点名)
- synchronized 不是读写锁。
七,相关题目:
-
- 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁:当多个线程同时访问同一个共享变量的时候,总是认为会出现线程安全问题,无论什么时候都按最坏的情况来处理,每次都要加锁
乐观锁:当多个线程同时访问同一个共享变量的时候,认为大概率不会出现线程安全问题,直接访问数据,在访问的同时识别一下是否会出现线程安全问题。
- 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
- 2,介绍下读写锁?
读写锁:就是将读操作和写操作分别加锁
读和读之间不互斥
写和写之间互斥
读和写之间互斥
读写锁多用在 “ 频繁读,不频繁写”的场景中 - 3,什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
自旋锁:就是当一个线程加锁失败时再次重新尝试获取锁,不会阻塞等待退出线程调度,而是一直占据CPU,一直在无限循环,直到锁被释放,能第一时间获取锁
优点:没有放弃CPU资源,自旋锁能在锁释放之后,第一时间获取到锁,在锁持有比较短的场景下,非常有用。
缺点:它会一直占据CPU资源 - 4,synchronized 是可重入锁么?
是可重入锁。
可重入锁指的是连续两次加锁不会导致死锁。
(实现的方式是在锁中记录该锁持有的线程身份,以及一计数器,如果发现当前加锁的线程就是持有锁的线程,则直接计数自增)