这年头,连“锁”都开始有情绪了。。乐观锁 VS 悲观锁(CAS)
Posted 小乔不掉发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了这年头,连“锁”都开始有情绪了。。乐观锁 VS 悲观锁(CAS)相关的知识,希望对你有一定的参考价值。
我是目录:
锁的类型:
偏向锁、轻量级锁、重量级锁、自旋锁、独占锁、共享锁、公平锁、非公平锁
1、乐观锁 VS 悲观锁
乐观锁 和 悲观锁 都是实现锁的两种 设计思想(和语言无关)
(1)根据使用场景来设计:
- 同一个时间点,经常只有 一个线程 操作共享变量,适合 乐观锁(乐观的心态)
乐观锁是线程直接尝试修改变量操作(不会阻塞),通过 api 调用方法的返回值,知道是成功还是失败
(在 Java 多线程是基于 CAS 来实现的) - 同一个时间点,经常有 多个线程 操作共享变量,适合 悲观锁(悲观的心态)
悲观锁 是线程先加锁,后修改变量操作
(2)设计原理:
- 乐观锁:乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以 每次在拿数据 的时候都会 上锁,这样 别人 想拿这个数据就会 阻塞 直到它拿到锁
(3)引出的问题:
- 乐观锁 的问题:并不总是能处理所有问题,所以会引入一定的 系统复杂度
- 悲观锁 的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以 性能不高
因为悲观锁的效率不高,所以我们需要在安全的前提下,着重提高效率,所以我们主要来研究 乐观锁
2、CAS(乐观锁策略)(重重点)
Java 多线程的实现: 基于 CAS的方式:(compare and swap 比较并交换)
(1)什么是 CAS?
使用锁时,线程 获取锁 是一种 悲观锁 策略,即假设每一次执行临界区代码都会产生冲突,所以 当前线程获取到锁 的时候同时也会 阻塞其他线程 获取该锁。
而 CAS 操作(又称为 无锁操作)是一种 乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止
(2)CAS 的操作过程:(面试)
- 实现/原理:基于 unsafe 来实现,本质上基于 CPU 提供的接口保证线程安全修改变量
CAS比较交换的过程可以通俗的理解为 CAS(V,O,N),包含三个值分别为:
(V 内存地址存放的现在的值 实际值;O 预期的值(之前存的值)(旧值);N 要更新的 新值)
当 V 和 O 相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。V 和 O 不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。
当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择 挂起线程。(而未优化的Synchronized:选择阻塞未抢到锁的线程)
(CAS的实现需要硬件指令集的支撑)
(3)引出的问题:ABA的问题(面试)
- 问题:就是在上面操作的过程中第一种情况, V 和 O 相等的情况,可能会有这样的情况:一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。
- 解决方案:添加一个版本号(每次修改后,版本号 + 1)
(4)应用:自旋锁
引入:
按之间的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个事实,自旋锁诞生了
自旋锁机制: 只要没抢到锁,就死等
缺点:是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的
(5)原子性并发包下的 api
实现类: AtomicInteger、AtomicLong、AtomicBoolean等
原理:基于 unsafe 类实现的 cas 和 自选锁
好处:对比 synchronized ,线程不需要在用户态 和 内核态 切换,一直处于 运行态,效率更高
以上是关于这年头,连“锁”都开始有情绪了。。乐观锁 VS 悲观锁(CAS)的主要内容,如果未能解决你的问题,请参考以下文章