目录
1.常见的锁
1.乐观锁&悲观锁
2.轻量级锁&重量级锁
3.读写锁&普通互斥锁
4.自旋锁&挂起等待锁
5.可重入锁&不可重入锁
6.公平锁&非公平锁
2.CAS
1.什么是CAS
2.CAS的应用
1.实现原子类
2.实现自旋锁
3.synchronized用到的锁策略
1.synchronized实现的锁策略
2.加锁的工作过程
3.一些优化操作
1.锁消除
2.锁粗化
1.常见的锁
1.乐观锁&悲观锁
乐观锁:对运行环境持乐观态度,刚开始不加锁,当有竞争的时候再去加锁
悲观锁:对运行环境持悲观态,刚开始就直接加锁
举个例子:
同学 A 认为 " 老师是比较忙的 , 我来问问题 , 老师不一定有空解答 ". 因此同学 A 会先给老师发消息 : " 老师你忙嘛 ? 我下午两点能来找你问个问题嘛 ?" ( 相当于加锁操作 ) 得到肯定的答复之后 , 才会真的来问问题 . 如果得到了否定的答复 , 那就等一段时间 , 下次再来和老师确定时间 . 这个是悲观锁 .同学 B 认为 " 老师是比较闲的 , 我来问问题 , 老师大概率是有空解答的 ". 因此同学 B 直接就来找老师 .( 没加锁 , 直接访问资源 ) 如果老师确实比较闲 , 那么直接问题就解决了 . 如果老师这会确实很忙 , 那么同学 B 也不会打扰老师 , 就下次再来 ( 虽然没加锁 , 但是能识别出数据访问冲突 ). 这个是乐观锁 .
2.轻量级锁&重量级锁
轻量级锁:可以是纯用户态的锁,消耗的资源比较小
重量级锁:可能会调用到系统的内核态,消耗的资源比较多。
- CPU 提供了 "原子操作指令".
- 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
- JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
重量级锁 : 加锁机制重度依赖了 OS 提供了 mutex
- 大量的内核态用户态切换
- 很容易引发线程的调度
这两个操作 , 成本比较高 . 一旦涉及到用户态和内核态的切换 , 就意味着 " 沧海桑田 ".
轻量级锁 : 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成 . 实在搞不定了 , 再使用 mutex.
- 少量的内核态用户态切换.
- 不太容易引发线程调度
3.读写锁&普通互斥锁
读锁:共享锁,读与读可以同时拿到锁资源
写锁:排他锁,不能同时写写,写读或者读写
普通互斥锁:synchronized,只能有一个线程拿到锁资源,其它的要参与锁竞争,没有竞争到锁的时候就要阻塞等待。
比如教务系统 .每节课老师都要使用教务系统点名 , 点名就需要查看班级的同学列表 ( 读操作 ). 这个操作可能要每周 执行好几次 .而什么时候修改同学列表呢 ( 写操作 )? 就新同学加入的时候 . 可能一个月都不必改一次 .
4.自旋锁&挂起等待锁
自旋锁:不停的询问资源是否被释放,如果释放了第一时间可以获得锁资源
挂起等待锁:等待通知之后再去竞争锁,并不会第一时间获取到锁资源
举个例子:
自旋锁是一种典型的 轻量级锁 的实现方式.
优点 : 没有放弃 CPU, 不涉及线程阻塞和调度 , 一旦锁被释放 , 就能第一时间获取到锁 .缺点 : 如果锁被其他线程持有的时间比较久 , 那么就会持续的消耗 CPU 资源 . ( 而挂起等待的时候是 不消耗 CPU 的 )
5.可重入锁&不可重入锁
可重入锁:对于同一个锁对象可以加多次锁
不可重入锁:不能对同一个锁对象加多次锁
6.公平锁&非公平锁
公平锁:先排队等待的线程先获取到锁资源
非公平锁:没有先来后到的说法,谁抢到就是谁的
2.CAS
1.什么是CAS
CAS:全程Compare and swap,字面意思“比较并交换”,一个CAS涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B
1.比较A与V是否相等(比较)
2.如果比较相等,将B写入V(交换)
3.返回操作是否成功
CAS伪代码(工作流程):
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
用期望值与内存中的值比较,如果内存中的值与期望值相等,那么用swapValue覆盖内存中的值,如果期望值与内存中的值不等那么什么也不做。
2.CAS的应用
1.实现原子类
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
伪代码实现:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
假设两个线程同时调用getAndIncrement。
(1).两个线程都读取value的值到oldvalue中。
(2).线程1先执行CAS操作,由于oldvalue和value值相同,直接对value进行赋值
注意 :CAS 是直接读写内存的 , 而不是操作寄存器 .CAS 的读内存 , 比较 , 写内存操作是一条硬件指令 , 是原子的 .
(3)线程2再执行CAS操作,第一次CAS的时候发现oldvalue和value不相等,不能赋值,因此进入循环。在循环中重新读取value的值赋给oldvalue
(4)线程2接下来第二次执行CAS,此时oldvalue和value相等,于是直接进行赋值操作
(5)线程1和线程2返回各自oldvalue值即可。
2.实现自旋锁
自旋锁伪代码:
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;
}
}
3.synchronized用到的锁策略
通过以上的锁策略可以知道,synchronized在不同的时期可能会用到不同的锁策略。
1.synchronized实现的锁策略
①既是乐观锁也是悲观锁
②既是轻量级锁也是重量级锁
- 轻量级锁是基于自旋锁实现的
- 重量级锁是基于挂起等待锁实现的
③是普通互斥锁
④既是自旋锁也是挂起等待锁
⑤是可重入锁
⑥是非公平锁
1. 开始时是乐观锁 , 如果锁冲突频繁 , 就转换为悲观锁 .2. 开始是轻量级锁实现 , 如果锁被持有的时间较长 , 就转换成重量级锁 .3. 实现轻量级锁的时候大概率用到的自旋锁策略
2.加锁的工作过程
①偏向锁
第一个进行加锁的线程,优先进入偏向锁状态。
偏向锁不是真的 " 加锁 ", 只是给对象头中做一个 " 偏向锁的 标记 ", 记录这个锁属于哪个线程 .如果后续没有其他线程来竞争该锁 , 那么就不用进行其他同步操作了 ( 避免了加锁解锁的开销 )如果后续有其他线程来竞争该锁 ( 刚才已经在锁对象中记录了当前锁属于哪个线程了 , 很容易识别当前申请锁的线程是不是之前记录的线程 ), 那就取消原来的偏向锁状态 , 进入一般的轻量级锁状态 .偏向锁本质上相当于 " 延迟加锁 " . 能不加锁就不加锁 , 尽量来避免不必要的加锁开销 .但是该做的标记还是得做的 , 否则无法区分何时需要真正加锁 .
②轻量级锁
随着其它线程进入竞争,偏向锁状态被消除,进入轻量级锁状态(自适应的自旋锁)。此时的轻量级锁就是通过CAS来实现的。
- 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
- 如果更新成功, 则认为加锁成功
- 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
自旋操作是一直让 CPU 空转 , 比较浪费 CPU 资源 .因此此处的自旋不会一直持续进行 , 而是达到一定的时间 / 重试次数 , 就不再自旋了 .也就是所谓的 " 自适应 "
3.一些优化操作
1.锁消除
在写代码时,程序会加入synchronized来保证线程安全。
如果加了synchronized的代码块中,只有读操作没有写操作,JVM就认为这个代码块没有必要加锁,JVM运行的时候就会被优化掉,这个现象叫做锁消除。
2.锁粗化
一段逻辑中如果出现多次加锁解锁,编译器+JVM会自动进行锁的粗化。
执行一个业务逻辑发生了四次锁竞争,在保证程序正确的情况下,JVM会做出优化,只加一次锁,整个逻辑执行完后再释放,从而提高效率。
举个例子:
滑稽老哥当了领导 , 给下属交代工作任务 :方式一 :打电话 , 交代任务 1, 挂电话 .打电话 , 交代任务 2, 挂电话 .打电话 , 交代任务 3, 挂电话 .方式二 :打电话 , 交代任务 1, 任务 2, 任务 3, 挂电话 .显然 , 方式二是更高效的方案 .