目录
4.2.3 ReentrantLock与synchronized的对比
1.常见的锁策略
1.1 乐观锁VS悲观锁
乐观和悲观是“锁的一种特性”,它是一类锁,而不是一把具体的锁。
悲观乐观,是对后续锁冲突是否激烈(频繁)给出的预测:
如果预测接下来所冲突的概率不大,就可以少做一些工作,就称为 乐观锁
如果预测接下来所冲突的概率很大,就可以多做一些工作,就称为 悲观锁
1.2 重量级锁VS轻量级锁
轻量级锁,锁的开销比较小 - 乐观锁
重量级锁,锁的开销比较大 - 悲观锁
1.3 自旋锁VS挂起等待锁
自旋锁,就属于是一种轻量级锁的典型实现(往往是在纯用户态实现)
忙等,消耗cpu但是换来更快的响应速度。如果获取锁失败,⽴即再尝试获取锁,⽆限循环,直到获取到锁为⽌.第⼀次获取锁失败,第⼆次的尝试会在极短的时间内到来.
⾃旋锁是⼀种典型的轻量级锁的实现⽅式.
• 优点:没有放弃CPU,不涉及线程阻塞和调度,⼀旦锁被释放,就能第⼀时间获取到锁.
• 缺点:如果锁被其他线程持有的时间⽐较久,那么就会持续的消耗CPU资源.(⽽挂起等待的时候是不消耗CPU的).
挂起等待锁,就属于一种重量级锁的典型实现(要借助系统api来实现)
一旦出现锁竞争了,就会在内核中触发一系列的动作(比如让这个线程进入阻塞的状态,暂时不参与cpu调度)
1.4 读写锁
把加锁分为了两种 - 读锁和写锁
读加锁,读的时候,可以读,但是不能写;写加锁,写的时候,不能读,也不能写
两个线程加锁过程中:
1.读锁和读锁之间,不会有竞争 多线程读取同一个数据,没有线程安全问题的
2.读锁和写锁之间,有竞争
3.写锁和写锁之间,也有竞争
1.5 可重入锁VS不可重入锁
一个线程对同一把锁,连续加锁两次,不会死锁,就是可重入锁,会死锁,就是不可重入锁。
1.6 公平锁VS非公平锁
当很多线程去加一把锁的时候,一个线程能够拿到锁,其他线程阻塞等待。一旦第一个线程释放锁之后,接下来是哪个线程可以拿到锁呢?
公平锁:”先来后到“顺序
非公平锁:”均等“的概率,重新竞争锁
操作系统提供的加锁api是默认”非公平锁“
1.7 sychronized
对于”悲观乐观“ 、”轻量重量“、”自旋-挂起等待“都是自适应的
不是读写锁、是可重入锁、是非公平锁
2.CAS
2.1 概念
CAS: 全称Compare and swap,字⾯意思:”⽐较并交换“,
比如有一个内存,M 现在还有两个寄存器A,B 即CAS(M,A,B)
如果M和A的值相同的话,就把M和B里的值进行交换,同时整个操作返回true (交换的本质是为了把B赋值给M)如果M和A的值不同的话,无事发生,整个操作返回false
(一个cpu指令,就能完成上述交换的逻辑)
单个的cpu指令,是原子的!!就可以使用CAS完成一些操作,进一步的替换”加锁“
给线程安全的代码,引入了新的思路 - 基于CAS实现线程安全的方式,也称为”无锁编程“
2.2 特点
优点:保证线程安全,同时避免阻塞(效率)
缺点:a.代码会更复杂,不好理解
b.只能适合一些特定场景,不如加锁方式更普适
2.3 应用
a.实现原子类
例如,int进行++就不是原子的,分为三步走(load,add,save)
AtomicInteger,基于CAS的方式对int进行封装了,此时进行++,就是原子的了。
b.实现自旋锁
2.4 CAS的ABA问题
CAS也是多线程编程的一种重要技巧,虽然开发中直接使用CAS的概率不大,但是经常会用到一些内部封装 了CAS的操作
CAS这块还有一个关键问题:ABA问题
CAS进行操作的关键,是通过 值 ”没有发生变化“来作为”没有其他线程穿插执行“判定依据
但是这个判定方式,不够严谨
更极端的情况下,可能有另一个线程穿插进来,把值A->B->A
针对第一个线程来说,看起来好像是这个值没变,但是实际上已经被穿插执行了
ABA问题通常是不会有bug的,但是极端情况下不好说
总结:在实际开发中,一般不会直接使用CAS,都是封装好的。
面试中比较容易考到CAS,一旦考察到CAS,一定涉及到ABA问题
解决方案:
给要修改的值,引⼊版本号.在CAS⽐较数据当前值和旧值的同时,也要⽐较版本号是否符合预期.
• CAS操作在读取旧值的同时,也要读取版本号.
• 真正修改的时候,
◦ 如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1.
◦ 如果当前版本号⾼于读到的版本号.就操作失败(认为数据已经被修改过了).
2.5 面试题
a.讲解下你⾃⼰理解的CAS机制
全称Compare and swap,即"⽐较并交换".相当于通过⼀个原⼦的操作,同时完成"读取内存,⽐较是否相等,修改内存"这三个步骤.本质上需要CPU指令的⽀撑.
b. ABA问题怎么解决?
给要修改的数据引⼊版本号.在CAS⽐较数据当前值和旧值的同时,也要⽐较版本号是否符合预期.如果发现当前版本号和之前读到的版本号⼀致,就真正执⾏修改操作,并让版本号⾃增;如果发现当前版本号⽐之前读到的版本号⼤,就认为操作失败.
3.synchronized原理
synchronized几个重要的机制:a.锁升级 b.锁消除 c.锁粗化
3.1 锁升级
偏向锁,不是真正意义的锁,只是做了一个标记,能不加就不加,实际上是一个懒汉模式。
什么是偏向锁?
偏向锁不是真的加锁,⽽只是在锁的对象头中记录⼀个标记(记录该锁所属的线程).如果没有其他线程参与竞争锁,那么就不会真正执⾏加锁操作,从⽽降低程序开销.⼀旦真的涉及到其他的线程竞争,再取消偏向锁状态,进⼊轻量级锁状态.
3.2 锁消除
编译器的一种优化手段
编译器会自动针对你当前写的代码,做出判定,如果编译器觉得这个场景不需要加锁,此时就会把你写的synchronized给优化掉。
3.3 锁粗化
⼀段逻辑中如果出现多次加锁解锁,编译器+JVM会⾃动进⾏锁的粗化。
synchronized里头,代码越多,就认为锁的粒度越粗,代码越少,就认为锁的粒度越细。
4.JUC的常见类
4.1 Callable接口
是创建线程的一种方式,适合于,想让某个线程执行一个逻辑,并返回结果的时候
4.2 ReentrantLock
可重入互斥锁,和synchronized定位类似,都是用来实现互斥效果,保证线程安全
4.2.1 ReentrantLock的⽤法:
• lock():加锁,如果获取不到锁就死等.
• trylock(超时时间):加锁,如果获取不到锁,等待⼀定的时间之后就放弃加锁.
• unlock():解锁
4.2.2 特点
优势:
1.ReentrantLock,在加锁的时候,有两种方式:lock, trylock,给了咱们更多的操作空间
2.ReentrantLock,提供了公平锁的实现(默认情况下是非公平锁)
3.ReentrantLock,提供了更强大的等待通知机制,搭配了Condition类,实现等待通知的。
虽然ReentrantLock有上述优势,我们在加锁的时候,还是首选synchronized,因为ReentrantLock使用更复杂,尤其容易忘记解锁,另外synchronized还有一系列优化机制。
4.2.3 ReentrantLock与synchronized的对比
• synchronized是⼀个关键字,是JVM内部实现的(⼤概率是基于C++实现).ReentrantLock是标准库的⼀个类,在JVM外实现的(基于Java实现).
• synchronized使⽤时不需要⼿动释放锁.ReentrantLock使⽤时需要⼿动释放.使⽤起来更灵活,但是也容易遗漏unlock.
• synchronized在申请锁失败时,会死等.ReentrantLock可以通过trylock的⽅式等待⼀段时间就放弃.
• synchronized是⾮公平锁,ReentrantLock默认是⾮公平锁.可以通过构造⽅法传⼊⼀个true开启公平锁模式.
4.2.4 选择
如何选择使⽤哪个锁?
• 锁竞争不激烈的时候,使⽤synchronized,效率更⾼,⾃动释放更⽅便.
• 锁竞争激烈的时候,使⽤ReentrantLock,搭配trylock更灵活控制加锁的⾏为,⽽不是死等.
• 如果需要使⽤公平锁,使⽤ReentrantLock.
5.信号量Semaphore
用来表述”可用资源的个数“,本质上是一个计数器。
每次申请一个可用资源,就需要让计数器-1 P操作 acquire
每次释放一个可用资源,就需要让计数器+1 V操作 release
锁 本质上就属于一种特殊的信号量
锁就是可用资源为1的信号量。
加锁操作,P操作,1->0;
解锁操作,V操作,0->1;
开发中如果遇到了需要申请资源的场景,就可以使用信号量来实现了