目录
1.CAS
1.1什么是CAS
1.2CAS是怎样实现
1.3CAS的应用
1.3.1实现原子类
1.3.2实现自旋锁
1.3.3CAS引发的ABA问题
2.synchronized关键字背后的锁升级流程
2.1偏向锁
2.2轻量级锁
2.3重量级锁
2.4其他优化操作
2.4.1锁消除
2.4.2锁粗化
1.CAS
1.1什么是CAS
CAS:compare and swap,字面意思就是“比较并交换”。
CAS一般操作如下:
当多个线程同时对某个资源进行 CAS 操作,只能有一个线程操作成功,但是并不会阻塞其他线程 , 其他线程只会收到操作失败的信号。 可以将CAS理解为乐观锁的一种实现方式。线程并不会阻塞,不断尝试。
1.2CAS是怎样实现
针对不同的操作系统,
JVM
用到了不同的
CAS
实现原理,简单来讲:
java 的
CAS
利用的的是
unsafe
这个类提供的
CAS
操作;
unsafe 的
CAS
依赖了的是
jvm
针对不同的操作系统实现的
Atomic::cmpxchg
;
Atomic::cmpxchg 的实现使用了汇编的
CAS
操作,并使用
cpu
硬件提供的
lock
机制保证其原子
性。
简而言之,是因为
硬件予以了支持,软件层面才能做到
。
1.3CAS的应用
1.3.1实现原子类
标准库中提供了
java.util.concurrent.atomic
包
,
里面的类都是基于这种方式来实现的,
典型的就是
AtomicInteger
类
.
其中的
getAndIncrement
相当于
i++
操作
。
1.3.1实现原子类 标准库中提供了 java.util.concurrent.atomic 包 , 里面的类都是基于这种方式来实现的, 典型的就是 AtomicInteger 类 . 其中的 getAndIncrement 相当于 i++ 操作 。
1.3.2实现自旋锁
基于
CAS
实现更灵活的锁
,
获取到更多的控制权。
来看伪代码:
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
1.3.3CAS引发的ABA问题
解决方案:
给要修改的值 , 引入版本号 . 在 CAS 比较数据当前值和旧值的同时 , 也要比较版本号是否符合预期 CAS 操作在读取旧值的同时 , 也要读取版本号。 真正修改的时候 , 如果当前版本号和读到的版本号相同, 则修改数据 , 并把版本号 + 1. 如果当前版本号高于读到的版本号. 就操作失败 ( 认为数据已经被修改过了 ).2.synchronized关键字背后的锁升级流程
2.1偏向锁 第一个尝试加锁的线程 , 优先进入偏向锁状态 . 偏向锁不是真的 " 加锁 ", 只是给对象头中做一个 " 偏向锁的标记 ", 记录这个锁属于哪个线程 . 如果后续没有其他线程来竞争该锁 , 那么就不用进行其他同步操作了 ( 避免了加锁解锁的开销 ) 如果后续有其他线程来竞争该锁 ( 刚才已经在锁对象中记录了当前锁属于哪个线程了 , 很容易识别 当前申请锁的线程是不是之前记录的线程 ), 那就取消原来的偏向锁状态 , 进入一般的轻量级锁状态 . 偏向锁本质上相当于 " 延迟加锁 " . 能不加锁就不加锁 , 尽量来避免不必要的加锁开销 . 但是该做的标记还是得做的 , 否则无法区分何时需要真正加锁 . 举例理解:
2.2轻量级锁
随着其他线程进入竞争
,
偏向锁状态被消除
,
进入轻量级锁状态
(
自适应的自旋锁
)。
此处的轻量级锁就是通过
CAS
来实现
.通过
CAS
检查并更新一块内存
(
比如
null =>
该线程引用
)
如果更新成功
,
则认为加锁成功,
如果更新失败
,
则认为锁被占用
,
继续自旋式的等待
(
并不放弃
CPU).。
2.3重量级锁
如果竞争进一步激烈
,
自旋不能快速获取到锁状态
,
就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的
mutex .
执行加锁操作
,
先进入内核态
.
在内核态判定当前锁是否已经被占用
如果该锁没有占用
,
则加锁成功
,
并切换回用户态
.
如果该锁被占用
,
则加锁失败
.
此时线程进入锁的等待队列
,
挂起
.
等待被操作系统唤醒
.
经历了一系列的沧海桑田
,
这个锁被其他线程释放了
,
操作系统也想起了这个挂起的线程
,
于是唤醒
这个线程
,
尝试重新获取锁
.
2.4其他优化操作
2.4.1锁消除
编译器
+JVM
判断锁是否可消除
.
如果可以
,
就直接消除
.
什么是
"
锁消除
"
有些应用程序的代码中
,
用到了
synchronized,
但其实没有在多线程环境下
. (
例如
StringBuffer)
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个
append
的调用都会涉及加锁和解锁
.
但如果只是在单线程中执行这个代码
,
那么这些加
锁解锁操作是没有必要的
,
白白浪费了一些资源开销
2.4.2锁粗化
一段逻辑中如果出现多次加锁解锁
,
编译器
+ JVM
会自动进行锁的粗化
.
锁的粒度
:
粗和细
实际开发过程中
,
使用细粒度锁
,
是期望释放锁的时候其他线程能使用锁
.
但是实际上可能并没有其他线程来抢占这个锁
.
这种情况
JVM
就会自动把锁粗化
,
避免频繁申请释
放锁
.
2.4其他优化操作
2.4.1锁消除
编译器
+JVM
判断锁是否可消除
.
如果可以
,
就直接消除
.
什么是
"
锁消除
"
有些应用程序的代码中
,
用到了
synchronized,
但其实没有在多线程环境下
. (
例如
StringBuffer)
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个
append
的调用都会涉及加锁和解锁
.
但如果只是在单线程中执行这个代码
,
那么这些加
锁解锁操作是没有必要的
,
白白浪费了一些资源开销
2.4.2锁粗化
一段逻辑中如果出现多次加锁解锁
,
编译器
+ JVM
会自动进行锁的粗化
.
锁的粒度
:
粗和细
实际开发过程中
,
使用细粒度锁
,
是期望释放锁的时候其他线程能使用锁
.
但是实际上可能并没有其他线程来抢占这个锁
.
这种情况
JVM
就会自动把锁粗化
,
避免频繁申请释
放锁
.
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个
append
的调用都会涉及加锁和解锁
.
但如果只是在单线程中执行这个代码
,
那么这些加
锁解锁操作是没有必要的
,
白白浪费了一些资源开销
2.4.2锁粗化 一段逻辑中如果出现多次加锁解锁 , 编译器 + JVM 会自动进行锁的粗化 . 锁的粒度 : 粗和细 实际开发过程中 , 使用细粒度锁 , 是期望释放锁的时候其他线程能使用锁 . 但是实际上可能并没有其他线程来抢占这个锁 . 这种情况 JVM 就会自动把锁粗化 , 避免频繁申请释 放锁 .



