CAS
自旋锁
悲观者与乐观者的做事方式完全不一样,悲观者的人生观是一件事情我必须要百分之百完全控制才会去做,否则就认为这件事情一定会出问题;而乐观者的人生观则相反,凡事不管最终结果如何,它会先尝试去做,大不了最后不成功。
这就是悲观锁与乐观锁的区别,悲观锁会把整个对象加锁占为自有后才去做操作,乐观锁不获取锁直接做操作,然后通过一定检测手段决定是否更新数据。
底层原理
乐观锁的核心算法便是CAS
[Compare And Swap],
1.自旋锁
借助C语言
调用CPU
底层指令实现的,基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能 ,提供总线锁定和缓存锁定两个机制来爆照复杂内存操作的原子性
自旋锁的核心实现,便要依靠另一个核心类Unsafe
2.Unsafe
类
CAS
的核心类就是UnSafe
,来源于jdk
中rt.jar
包下的所有方法都是navive
修饰的一个类
native方法:java方法是无法直接访问底层系统的,需要通过native方法来访问
Unsafe
相当于一个后门,基于该类可以直接操作特定内存的数据,换言之,unsafe
的操作直接调用底层资源执行相应任务!
我们需要提供CAS
对象,自增偏移量[内存地址]和自增值,即可完成线程安全的自增操作
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe
类中getAndAddInt
方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
var1
:cas
操作的对象即AtmoticInteger
var2
:cas
操作对象的内存地址
var4
: 需要变动的数值
var5
: 根据内存地址偏移量var2
找出var1
对象在主内存中真实值用当前对象的值与
var5
的内存值进行比较:相同:更新
var5
+var4
,返回true
不相同:继续取值然后再比较,直到更新完成
总之,CAS是线程安全且高效的
处理逻辑
它涉及到三个操作数内存值、预期值、新值
当且仅当预期值和内存值相等之时才将内存值修改为新值
这样处理的逻辑是,首先检查某块内存的值是否和之前读取时的值一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间此内存值作为预期值,执行到某处时线程二决定将新值设置到内存块中,如果线程一在此期间修改了内存块,则通过CAS
即可以检测出来,假如检测没问题则线程二将新值赋予内存卡。
缺点
CAS
虽然搞笑的解决院子操作,但是仍然存在三大问题
1. ABA
问题
因为CAS
需要在操作时检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A
,变成了B
,又变成了A
,那么使用CAS
进行检查时会发现它的值没有发生变化,但是实际上却变化了。
解决方法:打标记/添加版本号,来获取最近一次的更新变化
public class AtomicMarkableReferenceTest {
private final static String A = "A";
private final static String B = "B";
private final static AtomicReference<String> ar = new AtomicReference<>(A);
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(Math.abs((int) (Math.random() * 100)));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ar.compareAndSet(A, B)) {
System.out.println("我是线程1,我成功将A改成了B");
}
}).start();
new Thread(() -> {
if (ar.compareAndSet(A, B)) {
System.out.println("我是线程2,我成功将A改成了B");
}
}).start();
new Thread(() -> {
if (ar.compareAndSet(B, A)) {
System.out.println("我是线程3,我成功将B改成了A");
}
}).start();
}
}
输出结果:
我是线程2,我成功将A改成了B
我是线程3,我成功将B改成了A
我是线程1,我成功将A改成了B
显然,线程1和线程2,其实有一方做了多余的操作,假设线程1和线程2的处理逻辑是一样的话,就会出大问题
打标记
使用AtomicMarkableReference
,A->B
不处理,B->A
打标记,线程1再执行A->B
时,检测到标记后不执行
public class AtomicMarkableReferenceTest {
private final static String A = "A";
private final static String B = "B";
private final static AtomicMarkableReference<String> ar = new AtomicMarkableReference<>(A, false);
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(Math.abs((int) (Math.random() * 100)));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ar.compareAndSet(A, B, false, true)) {
System.out.println("我是线程1,我成功将A改成了B");
}
}).start();
new Thread(() -> {
if (ar.compareAndSet(A, B, false, true)) {
System.out.println("我是线程2,我成功将A改成了B");
}
}).start();
new Thread(() -> {
if (ar.compareAndSet(B, A, ar.isMarked(), true)) {
System.out.println("我是线程3,我成功将B改成了A");
}
}).start();
}
}
输出结果:
我是线程2,我成功将A改成了B
我是线程3,我成功将B改成了A
显然,线程1已经察觉出值发生了变化,就不在做同样的逻辑操作了。
添加版本号
使用AtomicStampedReference
,每次再做A->B
操作时,根据传递预期的版本号信息来判断是否继续往下执行
private final static String A = "A";
private final static String B = "B";
private static AtomicInteger ai = new AtomicInteger(1);
private final static AtomicStampedReference<String> ar = new AtomicStampedReference<>(A, 1);
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(Math.abs((int) (Math.random() * 100)));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ar.compareAndSet(A, B, 1, 2)) {
System.out.println("我是线程1,我成功将A改成了B");
}
}).start();
new Thread(() -> {
if (ar.compareAndSet(A, B, ai.get(), ai.incrementAndGet())) {
System.out.println("我是线程2,我成功将A改成了B");
}
}).start();
new Thread(() -> {
if (ar.compareAndSet(B, A, ai.get(), ai.incrementAndGet())) {
System.out.println("我是线程3,我成功将B改成了A");
}
}).start();
}
输出结果:
我是线程2,我成功将A改成了B
我是线程3,我成功将B改成了A
很明显,已经阻止了ABA
情况的发生
2.循环时间长开销大
自旋如果时间太长不成功,会给cpu
造成一定的负荷
解决方法:和ABA中添加版本号类似,版本号达到一定次数时,主动停止
3. 只保证一个共享变量原子操作
这意味着CAS
在有多个变量值的情况下,是无法保证线程的安全问题
解决方法:使用AtomicReference对象将多个变量值放入到一个对象操作
使用场景
并发冲突少,竞争不激烈的情况下,使用CAS
是比较高效的,高并发情况下冲突概率上升,自旋效率较低