Java多线程常见面试题-第一节:锁策略CAS和Synchronized原理
Posted 我擦我擦
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程常见面试题-第一节:锁策略CAS和Synchronized原理相关的知识,希望对你有一定的参考价值。
文章目录
一:常见的锁策略
synchronized是什么锁:总的来说,synchronized是一把自适应锁,它既是乐观锁(基于自旋锁实现)也是悲观锁(基于挂起等待实现);即是轻量级锁也是重量级锁;不是读写锁,是普通互斥锁;是非公平锁;是可重入锁
(1)乐观锁与悲观锁
乐观锁:预测锁冲突的概率不高,因此所做工作可以简单一些
悲观锁:预测锁冲突的概率较高,因此所做工作就要负责一些
举个例子:同学A和同学B想请教老师问题
- 乐观锁:同学A认为“老师是比较闲的,现在去问问题,大概率有空解答”。因此同学A直接就会来找老师(没有加锁,直接访问资源)。如果老师确实比较闲,那么问题就直接解决了;如果老师比较忙,同学A不会打搅老师,会在抽空再来(虽然没加锁,但是能识别出数据访问冲突)
- 悲观锁:同学B认为“老师是比较忙的,现在去问问题,大概率没空解答”。因此同学B会给老师先发消息询问他是否有空。得到肯定回复之后,才会真的去问问题;如果得到否定回复,那么同学A就会抽空再商定时间
乐观锁与悲观锁并不能说谁优谁劣,它们有各自的适用场景
- 如果当前老师确实比较闲,那么使用乐观锁更合适,而使用悲观锁会让效率降低
- 如果当前老师确实比较忙,那么使用悲观锁更合适,而使用乐观锁会让同学“白跑几趟”,耗费资源
(2)读写锁
读写锁:线程在访问数据时主要会涉及两种操作,分为读和写,此时
- 两个线程如果读的是一个数据,没有线程安全问题
- 两个线程写同一个数据,存在线程安全问题
- 一个线程读另一个线程写,存在线程安全问题
所以读写锁会把读操作和写操作区别对待
- 读锁与读锁之间不会产生竞争
- 写锁和写锁之间有竞争
- 读锁和写锁之间有竞争
(3)轻量级锁和重量级锁
轻量级锁:加锁和解锁开销比较小,例如纯用户态的加锁逻辑
重量级锁:加锁和解锁开销比较大,例如进入内核态的加锁逻辑
(4)自旋锁
自旋锁:一般来说,线程在抢占锁失败之后就会进入阻塞状态,放弃CPU,需要过一段时间才能再次被调度。但其实,在大部分情况下,用不了多长时间这个锁便会释放,所以没必要立马放弃CPU,因此可以立即再尝试获取锁,无限循环,直到获取到锁为止,如下
while(抢锁(lock) == 失败)
自旋锁是一种典型的轻量级锁的实现方式
- 优点:没有放弃CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁
- 缺点:如果锁被其他线程持有的时间比较久,就会持续耗费CPU资源
(5)公平锁和非公平锁
公平锁:遵从先来后到原则,谁先来的谁就先获取到锁
不公平锁:不遵从先来后到原则,释放锁时任意一个线程都有可能获取到锁
(6)可重入锁和不可重入锁
见(Java高级教程)第一章Java多线程基础-第一节4:synchronized关键字(监视器锁monitor lock)和volatile关键字
二:CAS
(1)CAS是什么
CAS:全称为Compare and swap,翻译过来就是比较并交换。一个CAS会涉及到如下操作,假设内存中的原数据为 D D D,旧的预期值为 A A A,需要修改的新值为 B B B
- 比较 A A A与 V V V是否相等
- 如果想当,则将 B B B吸入V
- 返回此操作是否成功
使用伪代码可解释如下,但注意下面的操作是非原子性的,真实的CAS是依靠原子硬件指令完成的
boolean CAS(address, expectValue, swapValue)
if(&address == expectedValue)
&address = swapValue;
return true;
return false;
CAS可以视为是一种乐观锁,当多个线程同时对某个资源进行CAS操作时,只能有一个线程操作成功,但是并不会阻塞其他线程,因为其他线程收到的是操作失败的信号
(2)CAS应用
①:实现原子类
实现原子类:标准库中的java.util.concurrent.atomic
包,里面的类都是基于这种方式实现的。以最为典型的AtomicInteger
类为例,其中的getAndIncrement
相当于i++
操作
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.getAndIncrement();
其伪代码实现如下,注释中的文字可解释基本原理
class AtomicInteger
private int value;
public int getAndIncrement()
//相当于把内存中的value读到某个寄存器中
int oldValue = value;
//oldValue就是前面所说的新值
while(CAS(value, oldValue, oloValue+1) != true)
//如果相同,就设置新值
oldValue = value;
return oldValue;
②:实现自旋锁
实现自旋锁:基于CAS可以实现更加灵活的锁,获取到更多控制权,自旋锁伪代码如下
public class SpinLock
//当前这把锁是谁获取到的
private Thread owner = null;
//通过CAS看当前锁是否被某个线程持有
//比较owner与null,如果为null表示是无人获取,即解锁状态,此时
//此时把当前调用lock1的线程的值设置到owner里
//如果这个锁已经被别的线程持有,那么就自旋等待
while(!CAS(this.owner, null, Thread.currentThread()))
public void unlock()
this.owner = null
(3)CAS缺陷:ABA问题
ABA问题:有两个线程t1
和t2
,以及一个共享变量num
,初始值为A
。线程t1
想要使用CAS把num
的值改为C
,那么就需要先读取num
的值,记录到oldNum
变量中,然后使用CAS判定当前num
的值是否为A,如果是,则将其修改为C
。但这里存在一个问题,线程t1
的CAS是期望num
不变就修改,但num
无法保证在这个过程中没有被修改过(例如t2
有可能把num
从A
改到B
然后又改回了A
),所以t1
线程无法区分num
始终是A呢还是说它早已经历了一个变化的过程,已经不再是原来的那个num
了
当然,在绝大多数情况下,num
反复修改并不会 带来什么问题,但也不排除一些特殊情况,例如,你有1000的存款,想要取出500,但不小心连按了两下,此时就会创建出两个线程来并发执行-500的操作
-
在正常情况下
- 线程1获取到当前存款为1000,期望更新为500;线程1获取到当前存款为1000,期望更新为500
- 线程1执行扣款成功,存款此时为500;线程2阻塞
- 轮到线程2执行时,发现当前存款为500,与CAS中读到的那个100不相同,所以失败
-
在异常情况下
- 线程1获取到当前存款为1000,期望更新为500;线程1获取到当前存款为1000,期望更新为500
- 线程1执行扣款成功,存款此时为500;线程2阻塞
- 在线程执行2之前,你的朋友正好给你转账了500,账户余额又变为了1000
- 轮到线程2执行时,发现当前存款为1000,因此再次扣款
解决方案:给要修改的值引入版本号,在CAS比较value和oldvalue时,也要比较版本号是否符合预期。在真正修改的时候
- 如果当前版本号和读到的版本号相同,则修改数据,然后版本号+1
- 如果当前版本号高于读到的版本号,则认为数据已经被修改过了,操作失败
三:Synchronized原理
Synchronized加锁过程:JVM将Synchronized
分为了无锁、偏向锁、轻量级锁和重量级锁这4种状态,并根据具体情况,进行升级。所以它是一把自适应锁
- 如果当前场景中,锁竞争不激烈,则会以轻量级锁进行工作,保证线程能够第一时间拿到锁
- 如果当前场景中,锁竞争激烈,则会以重量级锁进行工作,使线程挂起等待
偏向锁:第一个尝试加锁的线程,优先进入偏向锁状态。偏向锁不是真的加锁,而是给对象头中做一个偏向锁标记,用于记录这个锁属于哪个线程
- 如果后续没有其他线程来竞争这个锁:那么自然而然避免了加锁解锁的开销
- 如果后续有其他线程来竞争这个锁:那么就很容易能判断出当前申请锁的是不是之前记录的那个线程,然后解除偏向锁状态,进入一般的轻量级锁状态
所以偏向锁本质就是延迟加锁,能不加锁就不加锁,从而避免不必要的加锁解锁开销
轻量级锁:随着其他线程加入竞争,偏向锁状态解除,进入轻量级状态(自适应的自旋锁),此处自旋锁就是通过CAS 来实现的
- 通过CAS检查并更新一块内存
- 如果更新成功则认为加锁成功
- 如果更新失败则认为锁被占用,于是自旋等待
重量级锁:如果竞争进一步加剧,自旋不能快速获取到锁状态,就会膨胀为重量级锁,此处重量级锁是通过内核中的mutex
实现的
- 执行加锁操作,先进入内核态
- 在内核态判定当前锁是否已经被占用
- 如果该锁没有占用,则加锁成功,并切换回用户态
- 如果该锁被占用,则加锁失败。此时线程进入锁的等待队列,挂起等待被操作系统唤醒
- 等待这个锁被其他线程释放了,操作系统也想起了这个挂起的线程,于是唤醒
这个线程,尝试重新获取锁
以上是关于Java多线程常见面试题-第一节:锁策略CAS和Synchronized原理的主要内容,如果未能解决你的问题,请参考以下文章