这里只是浅谈一下CAS机制,有机会的话后续会深入
背景
尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。这个时候我们想到了CAS
CAS 是 Compare and Swap的简写,意思在于先比较再交换。比较啥,交换啥,以及能做啥就是我们要了解的。
我们都知道
i++
这样的操作并不是原子操作,在多线程的情况下会丢失很多值。
那么我们可以用synchronized锁住整个方法,或者用原子类例如AtomicInteger
来进行操作。
比如
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); //效果等于count+1
jdk在java.util.concurrent.atomic.
包底下给我们提供了很多诸如此类的原子类
AtomicLong / AtomicBoolean / AtomicMarkableReference / AtomicStampedReference …
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
假设线程1准备将变量count的值从10,变到11。 而在这时有个线程2抢先将变量count变为11。 这个时候线程1继续往下走,Compare了内存地址中的值 11 和旧的预期值10 不一致,更新失败不会将旧值更新为要修改的新值11。 线程1会从头获取内存里count的值11,要修改的新值为12,假设这次并没有别的线程的干扰,那么线程1对比内存里的值和旧的预期值一致,将count由11修改为12。 这个线程1自发重试的行为称之为“自旋”。
注意: 比较内存中旧值和将新值写进内存,这个compareAndSwap的整个过程是原子操作,从而保证了CAS机制。
看个代码的例子
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class CasTest {
private AtomicInteger atomic = new AtomicInteger(0);
private int i = 0;
public static void main(String[] args) {
final CasTest cas = new CasTest();
List<Thread> ts = new ArrayList<Thread>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
cas.count();
cas.safeCount();
}
}
});
ts.add(t);
}
for (Thread t : ts) {
t.start();
}
// 等待所有线程执行完成
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomic.get());
System.out.printf("耗时%s ms\n", System.currentTimeMillis() - start);
}
/**
* 使用CAS实现线程安全计数器
*/
private void safeCount() {
for (; ; ) {
int i = atomic.get();
boolean suc = atomic.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
/**
* 非线程安全计数器
*/
private void count() {
i++;
}
}
测试结果应该是
980212
1000000
耗时80 ms
重点理解一下safeCount方法里面的代码
原子类的底层是由unsafe实现的
什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
至于valueOffset对象,是通过unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单地把valueOffset理解为value变量的内存地址。
之前提到CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
而unsafe的方法参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。
正是unsafe的compareAndSwapxxx方法保证了Compare和Swap操作之间的原子性操作。
假设小明银行账户有100块钱。 今天的某一时刻小明正准备去取50块钱,然而因为不可抗力(网络波动,误操作,系统BUG等等)同时有两条线程发起了这个取钱的行为。 同时小明的妈妈也在给小明汇了50块钱。 按理说,最后的结果应该是小明取出50块钱,账户余额为100才对。 我们看看会发生什么…
线程名称 | 账户余额 | 期望余额 | 执行状态 |
---|---|---|---|
小明(取钱正常) | 100 | 50 | 成功 |
小明(取钱异常) | 100 | 50 | BLOCKED |
然后这时候余额是50。
小明妈汇钱进来了。。。
线程名称 | 账户余额 | 期望余额 | 执行状态 |
---|---|---|---|
妈(打钱正常) | 50 | 100 | 成功 |
小明(取钱异常) | 50 | 50 | BLOCKED |
然后这时候余额是100。也正在这时,异常的线程从阻塞中恢复
| 线程名称 | 账户余额 | 期望余额| 执行状态 |
|—: | —|
|小明(取钱异常)| 100 | 50 | 成功|
最终小明取出50块钱,余额为50… 小明妈白打了50块钱。
正确的操作应该是异常线程醒来后发现虽然当前钱是100,但我不能继续进行-50的操作。因为你这个-50的操作是上个版本的事情,已经过去了,应该被忽略。
有没有解决方案呢?
加个版本号之类的东西。
当我们执行compare的时候,不仅仅要比较内存里的值是否和旧的预期值相同,还要看版本号,比如当前值已经进行了N次修改版本号为N,而你这个线程手里握的版本号还是1… 那么即便内存里的值是否和旧的预期值相同,也要放弃这次操作。
应用实例 AtomicStampedReference 等。
总结一下:
- Java语言CAS底层如何实现?
利用unsafe提供了原子性操作方法。
什么是ABA问题?怎么解决?
当一个值从A更新成B,又更新会A,普通CAS机制会误判通过检测。
利用版本号比较可以有效解决ABA问题。
第一步: A1 -> B2 小明取钱 100变50
第二步: B2 -> A3 妈给小明打了50,钱又变成了100
在第一步产生的异常线程还是A1, 然而现在实际的值是A3。版本号对不上,于是不会再误操作变成B(不会误操作由100元再变成50元)
https://www.jianshu.com/p/736c532869a3