
一、常见的锁策略
1.1 悲观锁 vs 乐观锁
悲观和乐观是指锁竞争的激烈情况。
- 悲观锁:加锁的时候预测接下来的锁竞争会非常激烈,就需要针对这样的激烈情况额外做工作;
- 乐观锁:加锁的时候预测接下来的锁竞争不激烈,就不需要额外做工作去处理锁竞争情况;
1.2 重量级锁 vs 轻量级锁
这两种锁就对应上诉的悲观乐观情况下的处理机制。
- 重量级锁:应对上面的锁竞争激烈的悲观情况,效率更低;
- 轻量级锁:应对上面的锁竞争不激烈的情况,效率更高。
1.3 挂起等待锁 vs 自旋锁
这又是对上面的重量级与轻量级锁的典型实现。
- 挂起等待锁:重量级锁的典型实现,是操作系统内核级别的,加锁发生竞争,线程进入阻塞后,就需要内核进行唤醒。获取锁的周期会变长,但是这期间不会消耗CPU资源。
- 自旋锁:轻量级锁的典型实现,是应用程序级别的,加锁时发生竞争,一般不进行阻塞,而是通过忙等,等待后续程序唤醒。获取锁的周期很短,可以及时获取到锁,但是这期间会一直消耗CPU资源。
1.4 普通互斥锁 vs 读写锁
这两种锁是针对加锁解锁时的线程安全问题。
- 普通互斥锁只有加锁,解锁操作,并且读操作不会出现线程安全问题;
- 而读写锁有读加锁,写加锁,和解锁操作,
读锁与读锁之间不互斥;
读锁与写锁之间存在互斥;
写锁与写锁之间也存在互斥。
读写锁主要是针对读操作多,写操作少的情况服务。
1.5 可重入锁 vs 不可重入锁
这组锁就是针对同一个线程多次嵌套获取同一把锁。
- 可重入锁:字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
- 不可重入锁:字面意思是“不可以重新进入的锁”,即同一个线程多次获取同一把锁,会报错。
1.6 不公平锁 vs 公平锁
这组锁是针对线程获取锁的概率设置的。
- 不公平锁:在Java中不公平锁是概率均等的随机分配锁;
- 公平锁:在Java中公平锁是按照等待的时间先来后到分配锁。因为操作系统是随机调度,为了实现公平锁就需要数据结构来记录先后调度顺序。
二、synchronized特性
2.1 synchronized的锁策略
锁策略如下:
- synchronized是一个自适应的锁,开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁。
- 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁。
- 是一种普通互斥锁。
- 是一种不公平锁。
- 是一种可重入锁。
2.2 synchronized加锁过程
synchronized的自适应过程称为锁升级:无锁 -> 偏向锁 -> 自旋锁 -> 重量级锁。
-
偏向锁:偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.
如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)
如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别
当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.
就像跟你搞暧昧,但是不正式跟你确认关系,如果没人来追求你就一直不确定关系,如果有人追求你就确认关系。 -
无锁 -> 偏向锁 :这个阶段是进入synchronized代码块;
-
偏向锁 -> 自旋锁:这个阶段是拿到偏向锁后,遇到其他线程来竞争这个锁;
-
自旋锁 -> 重量级锁:这个阶段是JVM发现当前的锁竞争非常激烈。
2.3 其它优化措施
除了上面的synchronized锁升级以外,还有以下的优化措施:
-
锁消除:编译器优化措施,编译器会对加锁的代码进行判断(但这种判断是非常保守的,只有100%确定当前是单线程),如果当前逻辑不需要加锁,编译器就会自动去除synchronized。
-
锁粗化:一个代码对细粒度的代码反复加锁解锁,就会将这个步骤优化为更粗粒度的加锁解锁。
-
锁的粒度:加锁和解锁之间,包含的代码执行的逻辑/时间越多,则锁的粒度就越粗,反之越细。
三、CAS
3.1 CAS概念
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(交换)
- 返回操作是否成功。
伪代码表示如下:
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;