文章目录
一、常见的锁策略
当前此处介绍的这些所策略不仅仅局限于 Java,任何与锁相关的特性都与这些相关。
1.乐观锁 & 悲观锁
这里要注意的是,此处说的锁并不是单指一把锁,而是一类锁。
- 乐观锁: 通常假设不会发生冲突,只有在数据提交更新时才会对冲突进行检测,如有冲突就会返回信息,交给用户处理。
- 悲观锁: 通常假设最坏的情况,冲突一直存在,认为每次获取数据是都会有人修改,所以每次获取数据时都会加锁。
总的来讲,就是两者对锁的竞争激烈程度的认知不同。
synchronized 既是一个悲观锁,又是一个乐观锁。
默认是乐观锁,但是发现所竞争比较激烈时,就会变成悲观锁。
2.轻量级锁 & 重量级锁
- 轻量级锁: 轻量级加锁的开销较小,效率更高。(进行了少量内核态用户切换)
- 重量级锁: 重量级加锁的开销较大,效率更低。(进行了大量内核态用户切换)
这里说明一个比较常见的情况(多数情况下):
多数情况下,乐观锁,是一个轻量级锁。
悲观锁,是一个重量级锁。
synchronized 既是一个轻量级锁,又是一个重量级锁。
默认是轻量级锁,当锁竞争比较激烈时,就会转换为重量级锁。
3.自旋锁 & 挂起等待锁
首先说明一下这两类锁的特性;
自旋锁,是一种典型的轻量级锁。
挂起等待锁,是一种典型的重量级锁。
下面我通过举例来解释两类锁的特点:
首先设想一个场景,这里我们邀请三为人选,分别是: 喜羊羊,美羊羊,沸羊羊。
此时,沸羊羊 向 美羊羊 表白(尝试对美羊羊加锁),但是,美羊羊说,沸羊羊你是个好人,但是我有男朋友了(说明此时 喜羊羊 对美羊羊已经加锁)
呢么,沸羊羊想要上位,就只能等待(锁释放),于是有下面两种。
- 针对自旋锁: 每天都去问候美羊羊,时刻关心她,一但她和喜羊羊分手,可以第一时间得知。快速尝试获取锁,并建立关系。
很明显,自旋锁的形式,占用了大量的资源。 - 针对挂起等待锁: 沸羊羊被婉拒后,暗下决心,表示我愿意等,只要你幸福开心,我就躲起来不打扰你,如果哪一天想起我,你就告诉我。
这样就非常不确定,当 美羊羊 分手了,可能想起来沸羊羊,求安慰。但是,大概率根本不会想到。
所以对于挂起等待锁,当真正被唤醒的时候就可能已经沧海桑田了。但是优点也就是相对节省资源。
synchronized 这里的轻量级锁,是基于自旋锁的形式实现。
synchronized 这里的重量级锁,是基于挂起等待锁实现的。
4.互斥锁 & 读写锁
- 互斥锁: 就是提供 加锁 和 解锁 两个操作。当一个线程加锁了,另一个线程也尝试加锁就会阻塞等待。
- 读写锁: 提供三种操作
1.针对读加锁
2.针对写加锁
3.解锁
上面的三种操作,针对的是多线程对同一个变量并发写操作,读操作没有线程安全问题。
即就是,读锁与读锁之间没有互斥,写锁与写锁之间存在互斥,写锁与读锁之间存在互斥。
synchronized 不是读写锁
5. 公平锁 & 非公平锁
在这里,我们将公平定义为先来后到
同样,这里可以设定一个场景,此时假设美羊羊分手了。
公平锁: 当美羊羊分手后,由等待队列中最早的舔狗上位。(阻塞队列中的元素根据顺序依次获取锁)
- 非公平锁: 三个人都不等了,开始上去争抢,各凭本事。(阻塞队列中的元素竞争获取锁)
synchronized 是非公平锁。
二、CAS
1、什么是 CAS
CAS:全称为Compare and swap,字面意思: 比较并交换
假设内存中的数据 V,旧的预期值 A,需要修改的新值 B。
- 比较 A 和 V 的值是否相同。(比较)
- 若比较的值相同,将 B 写入 V。(交换)
- 返回操作是否成功。
如图所示:
这里需要注意的是,CAS 的这个过程,并非是一段代码实现,而是通过 一条 CPU 指令实现。
也就是说 CAS 操作是原子性的,这样就可以在一定程度上回避线程安全问题。
2. CAS 的应用场景
- 实现原子类
因为是原子类,真实的 CAS 是一个原子硬件指令来完成的,实现的是 i++ 这样的操作,这里只能使用伪代码来辅助理解,如图:
如上图所示,设定两个线程分别实现自增。
此时假设线程1 优先抢占CPU
此时,线程2进入 CPU
最后返回线程各自的 oldvalue 即可!
总的来说,CAS 就是 CPU 提供给我们的一种特殊指令,通过这个指令,可以再一定程度上处理线程安全问题。
2.实现自旋锁
同样,自旋锁也是以伪代码的形式展现。如图所示:
3. CAS 中的 ABA 问题
什么是 ABA 问题
我们已知,CAS 在运行中,就是检查 value 和 oldvalue 是否一致。如果一致,就视为 value 的值中途没有被修改过,所以下一步交换没有问题。
需要注意的是这里的 “一致”,可能是没有改过,也可能是 改过,但是又还原回来了。
通俗来讲,就是,我买了个手机,这个手机可能是新机,也可能是翻新机。这里我们不专业,无法区分!
ABA 这样的问题,在大部分情况下影响都不大。但是,仍然有极端情况不容忽视,问题如下:
假设到 ATM 上取钱,在取钱的时候,按下取钱键的一瞬间,机器故障卡了,此时我又不耐烦的多按了几下,此时就可能产生 bug,造成重复扣款的情况,如图:
解决方案
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
- CAS 操作在读取旧值的同时, 也要读取版本号.
- 真正修改的时候,
1.如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
2.如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).
三、 Synchronized 原理
目前,我们知道 synchronized 的最基本用法是,两个线程针对同一个对象加锁,会产生阻塞等待。
对于 synchronized 内部,其实还有很多优化机制,存在的目的就是为了让锁更高效。
- 锁升级/锁膨胀
当代码执行到 synchronized 代码块中实现加锁可能会经历下面几个过程,如图:
- 偏向锁: 进行加锁时,首先会进入到偏向锁这个状态。
这里并不是真正的加锁,而是先占一个位置,如有需要就真加锁,无需要就放弃。 - 轻量级锁: 当发生锁竞争的时候,就会从偏向锁升级为轻量级锁。
此时 synchronized 会通过 自旋 的方式进行加锁。 - 重量级锁: 在 synchronized 进行自旋时,内部有个计数器,当自旋到一定时间后,就会自动升级成重量级锁。
此时锁的类型是 挂起等待锁 ,是基于操作系统原生的 API 进行加锁了。
呢么有个问题,尽然能实现锁升级,呢么可不可以降级?
答案是不行。 在 JVM 的主流实现中,只有锁升级,没有锁降级。只要是锁对象,一旦被升级,就不能再回头了。
- 消除锁
编译器的智能判定,看当前的代码是否真的需要加锁,如果不需要,但是程序员加了,就自动将锁消除。 - 锁粗化
锁的粒度:synchronized 所包含的代码越多,粒度就越粗,包含的越少,粒度就越细。
通常情况下,一般认为锁的粒度细一点较好。
对于这里的内容,本人整理的十分有限,不足的地方还希望大家多多指点。