浅谈java中的悲观锁,乐观锁以及CAS操作
了解volatile的同学一定知道,volatile 可以保证可见性,但是它无法保证原子性。
所谓原子性,就是一个(一系列)操作,要么全都执行,要么全都不执行,不能执行到中间某种状态就结束,同时对于外界(其它)来看,要么就是看到执行前的结果,要么就是执行后的结果,不能看到中间状态。
举一个经典的例子:多线程对于全局volatile 变量的累加,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )代码如下:
1 public class Main {
2 static volatile int count = 0;
3 static final int TOTAL = 10000;
4
5 public static void main(String[] args) throws InterruptedException {
6 Runnable r = () -> {
7 for (int i = 0; i < TOTAL; i++) {
8 count++;
9 }
10 };
11
12 Thread t1 = new Thread(r);
13 Thread t2 = new Thread(r);
14 t1.start();
15 t2.start();
16
17 t1.join();
18 t2.join();
19
20 System.out.println("echo :" + count);
21 }
22 }
这个代码的执行结果如下,多次执行也基本不会达到目标值20000
1 Connected to the target VM, address: '127.0.0.1:54088', transport: 'socket'
2 echo :13533
3 Disconnected from the target VM, address: '127.0.0.1:54088', transport: 'socket'
产生这个问题的原因是,我们在处理自增操作时,它不是原子性的。
虽然两个线程对于这个变量的操作变化都是实时感知的,读的都是是实时值,但是计算和回写时可能就会出问题了。
详细说下
A 线程 将变量x自增为1
B线程读取1 ,B线程计算+1时,得到结果是2(注意此时2存在临时变量中),在计算+1时,A线程已经继续自增变量x到2甚至3,4,5,6...
B线程回写临时结果到变量x ,此时覆盖了A的操作,x 又变为了2。
此时B线程的操作就是中间状态执行期间,被其它线程并发操作了。导致回写失败。
那怎么解决呢?最常规的办法就是加并发锁,将并发的片段同步成一个整体,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )执行期间,不允许其它线程同步操作。
这种常规的办法就是加锁,这种锁通常是指无论并发是否发生,我先加锁,保证我在执行期间肯定不受到干扰。我们将这种时刻防护并发保护数据安全的锁称之为悲观锁。
这就像是游客进入地铁闸机,不管有没有其他游客准备并行进入。闸机通道,每次只限一个人操作。

闸机的旋转门旋转 (加锁)
进入人 (数据操作)
离开闸机,进入景区 (解锁)
像传统的 synchronized ReentrantLock 等锁,都是悲观锁。都有典型的加锁解锁操作。

悲观锁锁常用于竞争激烈的并发场景下。
除了加并发锁还有啥办法呢?
还可以通过状态的变化来控制。就以我们这个例子来说。
因为出现问题的本质时因为发生了并发,我们只要判断并发有没有发生就可以。
如果并发没发生,我直接操作有没有锁无所谓,如果并发发生了,我看下对我的影响,如果对我有影响,我就认为这次操作失败了,重新操作试下。
我们观察有没有发生并发有两个点,开始和结束点,
<1>如果在开始点观察:其它线程有没有也同步读取数据。细想就发现这太难了,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )首先你要并发的观察所有cpu核的线程有没有读数据,这个挑战太大。而且别人可能只是简单的读取一下不操作,或者即使你能观察到,别的线程也可能先于你观察就已经读到数据了。
这显然不可行的。
<2>其次就是观察结束点,数据有没有改变。别人怎么读无所谓。这种显然是可以的。我们只要监控变量的值发生变法了没有即可判断是否发生了并发,从而判断是否可以继续写。
如果线程读取的是1,操作回写的结果是2(新值),它就可以在回写时,判断下回写要覆盖的值是不是1(旧值)。如果是则覆盖写入,如果不是,则认为并发失败,重新尝试写入(或进行其它失败策略)。
这种通过判断并发是否发生才进行操作的方式,我们称之为乐观锁。
乐观并发控制一般分为三个步骤:
(1)读取 read
(2)修改 modify (计算出目标值)
(3)校验并提交/写入 (Validate & Commit)

乐观锁常常用于低并发的场景中。因为它避免了悲观锁的状态切换,因此它的性能在低并发时更高,高并发下由于冲突较多,会导致比较次数较多,从而导致性能下降。
我们业务中最常见的乐观锁,一般是在数据库层面通过where 语句来实现,
比如下边这个语句
update status = '待支付'
from order
where status = '已下单'
当用户下单后,校验身份
订单状态从初始-->(check用户身份)-->已下单-->(锁定库存)-->待支付
每次订单状态机发生正常业务状态跳转时,都check状态是否是预定状态,但是此时用户又可以并发的去操作订单,如取消订单,这时状态机的正常业务状态跳转就要发现被并发修改了,进而失败退出。
这就是乐观锁的一种典型应用场景。
像例子中这种比较变量当前值是否是预期值,如果是,就将变量值赋为心值(预期值交换为新值),如果不是则不做操作的行为
我们称之为compare and swap 比较并交换,也就是大家常说的CAS.
CAS 是一种思路,也是乐观锁的一种实现方式,除此之外,还可以通过数据库主键控制,数据库版本号等方式来实现,但是本质都差不多,就是在写入时进行原子级别的比较并写入
java中已经通过unsafe类结合c++代码实现了CAS的能力,但是操作不太方便,因此JUC中atomic包下提供了各种原子类,如:
AtomicBoolean、AtomicInteger、AtomicReference 等。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )这些类可以直接用于业务代码的各种类型的原子操作类。(Atomic原子类/unsafe类的实现和使用,我会在后边的文章中专门讲解)
但是CAS操作本身是无法直接的解决ABA问题的。
什么是ABA问题,就是指CAS 在比较预期值时,虽然值等于预期值,但是可能已经发生并发了,
比如线程1发现变量值为A,
线程1准备将A调整为C,
此时发生了并发,其它线程将变量值调整为了B,因为某种原因调整回A
线程1CAS 写入变量时,A仍然等于预期值,但是已经不是原来的A了,此时再发生写入,可能会有异常或场景遗漏。
这种情况往往发生在变量值可以发生循环变化时,对于不会循环时,这个问题就不会产生。
常规的解决办法就是加入一个新的变量,如版本号,版本号和每次的变量值时一一映射关系。这样即使变量值循环回去,但是版本号只会递增不会循环。
一般的数值操作,即使有ABA场景的发生也不用担心,大部分由于最终一致性的情况,并不会对业务有什么冲击。只有很少的场景需要结合业务或者是对象内部变化,才会引发新的问题。
最后再说下很多人提到的CAS是无锁么?(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )并不是,CAS也是乐观锁的一种实现,也是锁,虽然我们没有显示的使用,但是内部在真正实现原子操作的那个时间段内还是需要通过各种状态、指令来控制住了并发。
比如通过数据库实现的CAS 乐观锁,那么在update时,一般会有行锁或者表锁。
通过atomic包下的原子类进行cas,虽然没有直接使用锁,但是在底层调用C++进而调用cpu指令cmpxchg时,还是通过lock 指令来锁定内存指令或者缓存行来保证控制并发。
因此CAS 肯定是用到了锁,但是对于应用层面的业务来说,感知不到锁。
浅谈java中的悲观锁,乐观锁以及CAS操作的更多相关文章
- 浅谈Java中的equals和==(转)
浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str ...
- 浅谈Java中的对象和引用
浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...
- 浅谈Java中的equals和==
浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: String str1 = new String("hello"); String str2 = ...
- 浅谈Java中的深拷贝和浅拷贝(转载)
浅谈Java中的深拷贝和浅拷贝(转载) 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: ...
- 浅谈Java中的深拷贝和浅拷贝
转载: 浅谈Java中的深拷贝和浅拷贝 假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(bool ...
- 【转】浅谈Java中的hashcode方法(这个demo可以多看看)
浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native i ...
- 浅谈Java中的final关键字
浅谈Java中的final关键字 谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来 ...
- 【转】浅谈Java中的hashcode方法
哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 根据这个 ...
- 浅谈Java中的栈和堆
人们常说堆栈堆栈,堆和栈是内存中两处不一样的地方,什么样的数据存在栈,又是什么样的数据存在堆中? 这里浅谈Java中的栈和堆 首先,将结论写在前面,后面再用例子加以验证. Java的栈中存储以下类型数 ...
- 浅谈Java中的hashcode方法(转)
原文链接:http://www.cnblogs.com/dolphin0520/p/3681042.html 浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地 ...
随机推荐
- AI之旅:Microsoft.Extensions.AI 送惊喜,Cnblogs.DashScope.AI 表支持
https://www.cnblogs.com/cmt/p/18577574 2024年10月8日,微软 .NET 官方博客发布了一篇博文 Introducing Microsoft.Extensio ...
- dapr微服务
https://dapr.io/ https://docs.microsoft.com/zh-cn/dotnet/architecture/dapr-for-net-developers/gettin ...
- 论文阅读-PANTHER: Private Approximate Nearest Neighbor Search in the Single Server Setting
1 介绍 首先是介绍一些概念: 最近邻搜索KNN:找到与查询点最接近的前k个点. 近似最近邻搜索:在大型高维数据库中,KNN的成本会很高,此时该问题通常会被放宽为近似最近邻搜索ANNS,允许以高概率返 ...
- Xcode8设置启动页
转载请注明出处!!! 在现如今几乎每个APP都有自己的启动页,启动页的设置方法是怎样的.1.方法一在xcode7之后新建工程中都会有一个LaunchScreen.storyboard的文件.这个是系统 ...
- 本心三十年:vivo Vision的三维世界里,藏着5亿人的下个日常
问界M8纯电版以35.98万元起售价悍然杀入40万级纯电市场,并且打破行业旧律实现纯电与增程的同价.延续问界M8增程版的爆款之势,上市2小时大定突破7000台. 爆款基因背后,是华为全栈技术与鸿蒙智行 ...
- 使用Tabs选项卡组件快速搭建鸿蒙APP框架
大家好,我是潘Sir,持续分享IT技术,帮你少走弯路.<鸿蒙应用开发从入门到项目实战>系列文章持续更新中,陆续更新AI+编程.企业级项目实战等原创内容.欢迎关注! ArkUI提供了很多布局 ...
- Ai元人文:价值共生时代的技术哲学构想之宣言
Ai元人文:价值共生时代的技术哲学构想之宣言 宣言:价值共生时代的技术哲学构想 图片 缘起:对价值对齐范式的根本性质疑 当前人工智能陷入价值对齐的困境--试图将人类多元价值压缩为机器可优化的单一目标函 ...
- 模拟集成电路设计系列博客——4.1.1 Gm-C滤波器基本单元
4.1.1 Gm-C滤波器基本单元 积分器是大部分连续时间滤波器的主要组成单元.为了实现\(G_m-C\)滤波器中的积分器,可以使用如下图所示将一个跨导器和一个电容进行连接.跨导器首先是一个跨导单元( ...
- 在Linux系统上一键配置DoH,解决DNS解析被污染
前言 最近我的 swag 服务突然证书 renew 失败 诊断了一下发现原来是无法解析 acme-v02.api.letsencrypt.org 域名 换了几个 DNS 都不行,应该是 DNS 被污染 ...
- 数据采集卡设计方案:308-8路24bit 震动音频数据采集千兆网络传输卡
一款为测试音频和振动信号而设计的高精度数据采集卡.提供4/8路同步模拟输入通道,24bit分辨率, 单通道采样速率最高216KSPS.每通道集成独立的IEPE激励源,可实现加速度传感器及麦克风等相关的 ...