CAS 的全称是 Compare and swap 直译过来就是 比较并交换。
其实 CAS 是一个原子的硬件指令去完成比较并交换这个操作的,也就是 CAS 是 CPU 提供的一个特殊指令!
既然是原子的,也就是说 CAS 是可以保证线程安全的!
这里就来介绍下 CAS 的简单逻辑:
假设内存中的原数据为 V,旧的预期值是 A,需要修改的新值是 B (A和B是寄存器中的值)
比较 A 与 V 是否相等 (compare)
如果 A 与 V 相等,就将 B 写入 V (swap),不相等则无事发生
返回当前的操作是否成功
其实 CAS 就是这么简单,看到这,你还不理解有点蒙蒙的很正常!接着往下看。
回顾我们之前写过的一段代码:
public class Test {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
// 第一次执行结果: 64628
// 第二次执行结果: 62853
// 第三次执行结果: 53330
大概就是两个线程并发的修改同一个变量的内容,引发的线程安全问题。
之前的方案是采用加锁来解决的,但是现在也可以使用 Java 提供的原子类保证上述代码的原子性了。
原子类,底层自增采用了 CAS 来保证原子性,下面我们就来简单的认识下 Java 提供的原子类:
在标准库中提供了 java.util.concurrent.atomic包,这里面实现的类都是基于 CAS 这种方式实现的,其中典型的就是 AtomicInteger 类,此时我们就能通过这个 AtomicInteger 类来改造上面的代码:
public class Test {
private static AtomicInteger count = new AtomicInteger(0); // 初始值给 0
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
count.getAndIncrement(); // 相当于 count++
// count.incrementAndGet(); // 相当于 ++count
// count.getAndDecrement(); // 相当于 count--
// count.decrementAndGet(); // 相当于 --count
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5_0000; i++) {
count.getAndIncrement(); // 相当于 count++
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
// 程序执行结果: 100000
本质上 getAndIncrement() 方法就是将 count++ 的操作转成了原子的,那么这个方法底层是如何使用 CAS 的呢?
这里我们就写一段伪代码来大致的了解下 getAndIncrement 方法的实现:
// 下述为伪代码, 并非真实代码
private int value;
public int getAndIncrement() {
int oldValue = value; // oldValue 理解成寄存器 A
while (CAS(value, oldValue, oldValue + 1) != true) {
oldValue = value;
}
return oldValue;
}
这里我们就要了解 CAS(, , ,) 这里面三个参数的含义了,第一个参数 value 表示 V,第二个参数 oldValue 表示A,第三个参数表示 B。
value 是内存中的值,我们再来回顾下 CAS 的工作,如果 V 和 A 相等,就将 B 的 值与 V 的值交换,本质就是把 B 的值写入内存了,并返回 true,如果 V A 不相等返回 false!
正常情况下,value 和 oldValue 的值应该都是一样的吧!
但是我们要考虑多线程的情况,假设 t1 线程执行完 oldValue = value; 这一行的时候,CPU 去执行 t2 线程了,t2 修改了 value 的值,此时 CPU 又回来执行 t1 线程了,进入 while 循环后执行 CAS 操作(注意!CAS 操作是原子的),要重新读取 value 值,此时就能发现 value 值被改了,于是就能得出 V 和 A 不相等,于是返回 false,循环也就不会结束,此时就会重新将 value 的值赋值给 oldValue。由此一来,就能保证线程安全了!
原子类这里的实现,就是每次修改 value(内存) 之前,都会确认一下要修改的值是否改变了!
这里我们简单写一个实现自旋锁的伪代码:
private Thread owner = null;
public void lock() {
while (!CAS(this.owner, null, Thread.currentThread())) {
}
}
public void unlock() {
this.owner = null;
}
相信这段代码也不难理解,lock() 方法就是加锁操作,会一直判断 owner 是否为 null,如果 V 和 A 相等,也就是该对象没有被加锁,此时就获取当前调用该方法的线程,令 owner = 当前线程对象,此时就相当于该线程获取到锁了。
如果 V 和 A 不相等,表示 owner 里面存了其他线程对象(其他线程已经获取到锁了),此时 owner 也和 null 不相等了,此时也就不会交换 B 与 V 的值,直到 unlock() 后,才有可能进行交换了。
上述就是自旋锁逻辑上的实现。
注意!上述原子类和自旋锁的伪代码中的 CAS 并不是 Java 给我们提供的方法,Java 原生的 CAS 方法比较复杂,此处相当于是一个简化的方式。
这个 ABA 问题也是面试中常见的一个问题,CAS 在运行中的核心就是检查 value 和 oldValue 的值是否一致,如果一致就视为 value 中途没有被改变,所以就可以进行下一步的交换操作。
但是我们仔细想想,会不会 value 被改变了,但又被还原回来了?此时细思极恐!
这就比如,张三之前谈过十几次恋爱,但对每一任都说之前没有谈过恋爱,把自己包装成恋爱小白,只要张三不露馅,所以张三的每一任都认为张三是初恋,这样问题可就大了呀!
ABA 问题大部分情况下都不会对代码/逻辑产生影响,但是不能排除一些极端情况!
此时我们就举一个不太现实的例子,但是能让大家更好的理解 ABA 问题!
例:滑稽小伙要去取钱,假设滑稽小伙卡里只有 10000 元
滑稽小伙准备一下子取 5000 出来(此时 ATM 使用 CAS 的方式来扣款),可是按下取款的一瞬间,机器卡了,滑稽小伙就手欠又按了一下,如果机器出 bug 了,就会触发两次扣款。
由于是 CAS 实现的,不会有问题,因为第二次扣款发现余额不对,也就不会触发扣款!
但是万一第一次执行扣款后,第二次扣款前,滑稽表哥给滑稽转了 5000 块钱呢?
此时就扣款了两次,但滑稽只得到了 5000 块钱,还亏了 5000,那么这种概率非常非常低!但也不保证完全不会出现!
针对上述的问题如何解决呢?
我们可以引入一个版本号,设想一下,初始的版本号是1,每次修改版本号都 +1,然后进行 CAS 的时候,就不是以金额为基准判断了,而是以版本号为基准,如果版本号没有发生变化,也就说明金额也没发生变化,如果版本号变了,那金额肯定被修改过。
注意:版本号只能增加,不能减少!
所以乐观锁在访问同一个变量的时候,如何判断当前是否有冲突呢?其实也是通过引入版本号的方式来判断是否访问冲突了!
下期预告:【多线程】synchronized 原理