什么是CAS?
CAS全称“Compare and swap”,字面意思就是“比较并交换”
CAS有什么用?
实现了无锁编程,这就意味着减少了线程之间的阻塞和竞争。并且CAS操作是原子性的,保证了数据的一致性
工作原理
假设内存中的原数据为V,旧的预期值为A,新的需要修改的值为B
1.首先比较 V 和 A 的值是否相等
2.如果比较相等则将 V 的值更改为 B 的值
3.返回结果,如果更新成功返回 true,如果失败返回 false( V 的值和 A 不相等,即 V 已经被其他线程修改了)
针对第3点来说,既然CAS的操作是原子性的,那么为什么 V 会被其他的线程修改呢?
这是因为 CAS 操作只保证了比较和交换是原子性的,并不能保证在这之前和之后的操作是原子性的,可能在进行比较之前 V 的值已经被其他线程进行了修改,此时再和 A 进行比较那么因为值不相同所以就会返回 false
ABA问题
假定现在有一个共享数据,其初始值为A,线程1可能先读取到这个值A,但是在进行修改的时候,线程2插入进来了读取到了A的值并且修改成了B,然后又因为某种原因修改成了A,最终对于线程1来说这个数据是没有发生改变的
来举一个具体的场景吧
人物:滑稽1,滑稽2
场景:滑稽1 给 滑稽2 转账500元
初始值:滑稽1 账户1000元,滑稽2 账户1000元
正常情况:
1.线程1 执行了转账 500 元之后, 滑稽1 的账户余额就从 1000 变成了 500 ,线程2进入阻塞等待
2.轮到线程2 执行的时候发现滑稽1 的账户余额(500)与初始余额(1000)对不上号之后,就不再执行这个操作了
异常情况:
1.线程1 执行完转账操作之后,账户余额从1000变成了500,此时线程2进入阻塞等待中
2.在线程2执行之前,朋友C突然想起来还欠滑稽1 500元钱,于是给滑稽1 转账了500元,这时滑稽1的账户余额又从500变成了1000
3.当线程2开始执行的时候,比较账户余额和初始余额的值是相同的,发现相同,于是又进行了转账500的操作,所以滑稽1的账户余额又从1000变成了500,在这个过程中滑稽1 进行了两次转账操作,多转了500
那么针对上述这个异常情况的场景我们应该怎样解决呢?
版本号
针对上面这个场景我们可以引入版本号来解决?版本号有什么用?
- CAS在读取旧值的同时也要读取版本号
- 修改的时候:
- 1.如果当前版本号与读取到的版本号相同,那么修改数据,并把版本号+1
- 2.如果当前版本号大于读取到的版本号,就认为操作失败,因为数据已经被修改过了
情景演绎:
还是针对上述的场景:
1.我们将账户余额的版本号设为 1
2.线程1 执行转账操作之后,账户余额从1000变成了500,版本号从1变成2,线程2进入阻塞等待
3.朋友C给滑稽1 转账500 ,此时账户余额从500变成了1000,版本号从2变成了3
4.线程2开始执行转账操作,发现虽然账户余额(1000)与初始账户(1000)一样,但是当前版本号(3)大于读到的版本号(1),于是这个转账操作就不会进入
什么是synchronized
在上一篇博客中我们介绍到了什么是锁策略,那么现在来讲讲synchronized的基本原理
- 开始是乐观锁,如果锁冲突频繁则转化成悲观锁
- 开始是轻量级锁,如果锁被持有的时间比较长,那么就会转换成重量级锁
- 实现轻量级锁大概率用到的是自旋锁策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
加锁过程
JVM会将synchronized分为无锁,偏向锁,轻量级锁,重量级锁,并根据情况,依次进行升级
偏向锁
第一次尝试加锁的过程中,会先进入偏向锁转态,偏向锁并不是真的给线程进行加锁,而是相当于给线程做个标记,标记这个锁是属于哪个线程的,如果后续没有其他线程来竞争这个锁,那么就不用进行同步操作了(避免了加锁解锁的开销),如果后续有其他线程来竞争该锁,也能很容易识别当前申请锁的线程是不是之前进行标记的线程,如果不是那就取消偏向锁的状态进入轻量级锁,说白了偏向锁的本质就是“延迟加锁”,并不是真正意义上的加锁
轻量级锁
偏向锁升级成轻量级锁的过程发生在多个线程同时竞争一把锁的情况下,当一个线程已经获得了偏向锁,另一个线程试图获取该锁时,原先的偏向锁转态将被撤销,并转换成轻量级锁
升级过程:
- 如果该线程没有锁则JVM则会给该线程加上一把偏向锁,并将该线程的 Mark Word 设置 成偏向模式,同时记录下该线程的 ID
- 如果其他线程尝试获取该锁,JVM则会检查 Mark Word 中的线程 ID 是否与当前线程的 ID相同,如果不相同说明存在锁竞争问题
- JVM会先撤销原来的偏向锁,并将 Mark Word 从偏向模式变成无锁模式,这会让之前被记录的 ID 被清除
- 撤销偏向锁后会尝试,新的线程会尝试获取轻量级锁。轻量级锁尝试用 CAS 来获取该锁:
①新线程会尝试使用CAS将自己线程的 ID 写入 Mark Word中
②如果CAS成功,则说明没有其他线程竞争该锁,新线程获取轻量级锁
③如果CAS失败,则说明其他线程正在尝试获取该锁,或者该锁已经被其他线程锁拥有
5.如果多次CAS失败则会从轻量级锁变成重量级锁
重量级锁
如果竞争激烈,自旋不能快速获取到锁状态,则就会变成重量级锁
- 执行加锁操作,先进入内核态
- 在内核态判定当前锁是否已经被占用
- 如果该锁没有被占用,则加锁成功,并切换回用户态
- 如果该锁被占用,则加锁失败,此时线程进入等待锁队列,挂起,直到被操作系统唤醒