线程安全之乐观锁和悲观锁

锁可以从不同的⻆度分类。其中,乐观锁和悲观锁是⼀种分类⽅式。
悲观锁:
悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发
⽣冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同⼀时间只能有⼀
个线程在执⾏。
乐观锁:
乐观锁⼜称为⽆锁,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问
没有冲突,线程可以不停地执⾏,⽆需加锁也⽆需等待。⽽⼀旦多个线程发⽣冲
突,乐观锁通常是使⽤⼀种称为CAS的技术来保证线程执⾏的安全性。
由于⽆锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁天⽣
免疫死锁。
乐观锁多⽤于读多写少的环境,避免频繁加锁影响性能;⽽悲观锁多⽤于写多读
的环境,避免频繁失败和重试影响性能。
CAS的概念
CAS的全称是:⽐较并交换(Compare And Swap)。在CAS中,有这样三个值:
V:要更新的变量(var)
E:预期值(expected)
N:新值(new)
⽐较并交换的过程如下:
判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程
更新了V,则当前线程放弃更新,什么都不做。
所以这⾥的预期值E本质上指的是旧值
我们以⼀个简单的例⼦来解释这个过程:
1. 如果有⼀个多个线程共享的变量 i 原本等于5,我现在在线程A中,想把它设
置为新的值6;
2. 我们使⽤CAS来做这个事情;
3. ⾸先我们⽤i去与5对⽐,发现它等于5,说明没有被其它线程改过,那我就把
它设置为新的值6,此次CAS成功, i 的值被设置成了6
4. 如果不等于5,说明 i 被其它线程改过了(⽐如现在 i 的值为2),那么我就
什么也不做,此次CAS失败, i 的值仍然为2
在这个例⼦中, i 就是V5就是E6就是N
那有没有可能我在判断了 i 5之后,正准备更新它的新值的时候,被其它线程更
改了 i 的值呢?
不会的。因为CAS是⼀种原⼦操作,它是⼀种系统原语,是⼀条CPU的原⼦指令,
CPU层⾯保证它的原⼦性
当多个线程同时使⽤CAS操作⼀个变量时,只有⼀个会胜出,并成功更新,其余均
会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然
也允许失败的线程放弃操作。
前⾯提到,CAS是⼀种原⼦操作。那么Java是怎样来使⽤CAS的呢?我们知道,在
Java中,如果⼀个⽅法是native的,那Java就不负责具体实现它,⽽是交给底层的
JVM使⽤c或者c++去实现。
Java中,有⼀个 Unsafe 类,它在 sun.misc 包中。它⾥⾯是⼀些 native ⽅法,
其中就有⼏个关于CAS的:
当然,他们都是 public native 的。
Unsafe中对CAS的实现是C++写的,它的具体实现和操作系统、CPU都有关系。
LinuxX86下主要是通过 cmpxchgl 这个指令在CPU级完成CAS操作的,但在多处
理器情况下必须使⽤ lock 指令加锁来完成。当然不同的操作系统和处理器的实现
会有所不同,⼤家可以⾃⾏了解。
当然,Unsafe类⾥⾯还有其它⽅法⽤于不同的⽤途。⽐如⽀持线程挂起和恢复
park unpark LockSupport类底层就是调⽤了这两个⽅法。还有⽀持反射操
作的 allocateInstance() ⽅法。
这⾥介绍⼀下CAS实现原⼦操作的三⼤问题及其解决⽅案。
10.5.1 ABA问题
所谓ABA问题,就是⼀个值原来是A,变成了B,⼜变回了A。这个时候使⽤CAS
检查不出变化的,但实际上却被更新了两次。
ABA问题的解决思路是在变量前⾯追加上版本号或者时间戳。从JDK 1.5开始,
JDKatomic包⾥提供了⼀个类 AtomicStampedReference 类来解决ABA问题。
这个类的 compareAndSet ⽅法的作⽤是⾸先检查当前引⽤是否等于预期引⽤,并且
检查当前标志是否等于预期标志,如果⼆者都相等,才使⽤CAS设置为新的值和标
志。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
10.5.2 循环时间⻓开销⼤
CAS多与⾃旋结合。如果⾃旋CAS⻓时间不成功,会占⽤⼤量的CPU资源。
解决思路是让JVM⽀持处理器提供的pause指令。
pause指令能让⾃旋失败时cpu睡眠⼀⼩段时间再继续⾃旋,从⽽使得读操作的频
率低很多,为解决内存顺序冲突⽽导致的CPU流⽔线重排的代价也会⼩很多。
10.5.3 只能保证⼀个共享变量的原⼦操作
这个问题你可能已经知道怎么解决了。有两种解决⽅案:
1. 使⽤JDK 1.5开始就提供的 AtomicReference 类保证对象之间的原⼦性,把多个
变量放到⼀个对象⾥⾯进⾏CAS操作;
2. 使⽤锁。锁内的临界区代码可以保证只有当前线程能操作

你可能感兴趣的:(线程安全之乐观锁和悲观锁)