@[TOC]
CAS
CAS(Compare And Set)比较交换,是一种无锁算法。即不使用锁的方式来实现多线程同步。由于是无锁的策略,也就是在没有线程被阻塞的情况下实现变量同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS算法:CAS(V, E, N)。
V:要更新的变量。
E:期望值。
N:新值。
当V==E值时,才会将N赋值给V;如果V!=E时,说明已经有别的线程做了更新,当前线程什么都不做(一般是一种自旋的操作,不断的重试-----也是自旋锁)。
基于CAS的线程安全AtomicInteger
package com.thread.atomic;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by shamee-loop on 2020/12/19.
*/
public class AtomicIntegerDemo {
static AtomicInteger i = new AtomicInteger();
public static class AddThread implements Runnable {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
i.incrementAndGet();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int j = 0; j < 10; j++) {
threads[j] = new Thread(new AddThread());
}
for (int j = 0; j < 10; j++) {
threads[j].start();
}
for (int j = 0; j < 10; j++) {
threads[j].join();
}
System.out.println(i);
}
}
最终程序输出为100000。如果是线程不安全的情况下,输出的值应该是<100000的。
先来看AtomicInteger的incrementAndGet()方法实现:
这里的unsafe顾名思义是一个封装了不安全的操作的类。它是sun.misc包下的。这个类是封装了一些类似指针的操作(我们知道C或者C++的指针操作是不安全的,这也是java去除指针的原因,所以暂且这么理解吧)。
我们再跟进去源码:
可以看到源码356行,实际上使用了public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);是一个native方法。这里的参数含义:
var1:为给定的对象
var2:对象内的偏移量(其实是一个字段到对象头部的偏移量,可以通过这个偏移量来快速定位字段)
var3:期望值
var4:要设置的值
看到这里,其实就知道了compareAndSwapInt方法内部必然是使用CAS原子指令来完成的。
CAS优点
1、在高并发下,性能比锁好
2、避免了死锁的情况
CAS缺点
1、CPU开销大
这个很好理解,上面提到在V!=E的情况下,当前线程会通过自旋的方式来不断的重试,直到操作成功。如果长时间不成功,必然会给CPU带来非常大的开销。
2、只能保证一个共享变量的原子操作
CAS只对一个共享变量有效,当操作多个共享变量时,CAS无效。JDK1.5开始,添加了AtomicRefrence来保证对象引用之间的原子性。我们可以利用锁或者AtomicRefrence把多个变量放在一个对象里来进行CAS操作。
3、ABA问题
如果变量V的初始值是A,有个线程更新了V的值为B;此时,如果当前线程要读取变量V的时候,又有个线程将V的值改为A,这时候当前线程会误以为V是没有被修改过的(实际上被修改了两次,A->B->A)。这就是ABA问题。
举个栗子:
package com.thread.atomic;
import java.util.concurrent.atomic.AtomicReference;
/**
* 贵宾卡充值案例模拟。
* 当余额不足20的时候,充值20
* 另一条线程,当金额大于10的时候,消费10
* Created by shamee-loop on 2020/12/19.
*/
public class AtomicReferenceDemo {
static AtomicReference<Integer> money = new AtomicReference<>(15);
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Integer m = money.get();
new Thread() {
public void run() {
while (true) {
if (m < 20) {
if (money.compareAndSet(m, m + 20)) {
System.out.println("余额小于20,充值成功。余额=" + money.get());
break;
}
} else {
System.out.println("余额大于20,无需充值");
break;
}
}
}
}.start();
new Thread() {
public void run() {
while (true) {
Integer m = money.get();
if (m > 10) {
System.out.println("余额大于10");
if (money.compareAndSet(m, m - 10)) {
System.out.println("消费10元,余额=" + money.get());
break;
}
} else {
System.out.println("余额不足");
break;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
程序输出:
这里多充值了一次20。原因就是账户余额被反复修改,修改后值等于原来的值,误以为没有被修改过,所以导致CAS无法正确判断当前数据状态。
ABA问题解决
1、版本号机制
对象内部多维护一个版本号,每次操作的同时版本号+1;CAS原子操作时,不只是判断值的状态,也判断版本号是否等于原来的版本号;就算值相等,版本号不等,也判断为被线程修改过。
2、带有时间戳的对象引用 AtomicStampReference
package com.thread.atomic;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 贵宾卡充值案例模拟。
* 当余额不足20的时候,充值20
* 另一条线程,当金额大于10的时候,消费10
* Created by shamee-loop on 2020/12/19.
*/
public class AtomicReferenceDemo {
static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(15, 0);
public static void main(String[] args) {
Integer stamp = money.getStamp();
for (int i = 0; i < 3; i++) {
Integer m = money.getReference();
new Thread() {
public void run() {
while (true) {
if (m < 20) {
if (money.compareAndSet(m, m + 20, stamp, stamp + 1)) {
System.out.println("余额小于20,充值成功。余额=" + money.getReference());
break;
}
} else {
System.out.println("余额大于20,无需充值");
break;
}
}
}
}.start();
new Thread() {
public void run() {
while (true) {
Integer m = money.getReference();
Integer stamp = money.getStamp();
if (m > 10) {
System.out.println("余额大于10");
if (money.compareAndSet(m, m - 10, stamp, stamp + 1)) {
System.out.println("消费10元,余额=" + money.getReference());
break;
}
} else {
System.out.println("余额不足");
break;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
程序输出:
类似版本号机制,这里对象内部不仅维护了对象值,还维护了一个时间戳。当对应的值被修改时,同时更新时间戳。当CAS进行比较时,不仅要比较对象值,也要比较时间戳是否满足期望值,两个都满足,才会进行更新操作。
这是内部的实现方式。