1.C A S基本概念
C A S(Compare and swap)或者C A S(Compare and set或者比较交换,是一种无锁原子算法,映射到操作系统就是一条cmpxchg
硬件汇编指令(保证原子性),其作用是让C P U
将内存值更新为新值,但是有个条件,内存值必须与期望值相同,并且C A S
操作无需用户态与内核态切换,直接在用户态对内存进行读写操作(意味着不会阻塞/线程上下文切换。
一次C A S
的操作:包含3
个参数C A S(V,E,N)
,V
表示待更新的内存值,E
表示预期值,N
表示新值,当 V
值等于E
值时,才会将V
值更新成N
值,如果V
值和E
值不等,不做更新。
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:
最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试。
synchronized 是基于悲观锁的思想:
最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发, 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
C A S
需要额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的,如果变量不是你想象的那样,说明它已经被别人修改过了,你只需要重新读取,设置新期望值,再次尝试修改就好了。
2.C A S保证原子性
为了保证C A S
的原子性,C P U
提供了下面两种方式
-
总线锁定
-
缓存锁定
2.1总线锁定
总线(B U S
)是计算机组件间的传输数据方式,也就是说C P U
与其他组件连接传输数据,就是靠总线完成的,比如C P U
对内存的读写。
总线锁定:是指C P U
使用了总线锁,所谓总线锁就是使用C P U
提供的LOCK#
信号,当C P U
在总线上输出LOCK#
信号时,其他C P U
的总线请求将被阻塞。
2.2缓存锁定
总线锁定方式虽然保证了原子性,但是在锁定期间,会导致大量阻塞,增加系统的性能开销,所以现代C P U
为了提升性能,通过锁定范围缩小的思想设计出了缓存行锁定(缓存行是C P U
高速缓存存储的最小单位)。所谓缓存锁定是指C P U
对缓存行进行锁定,当缓存行中的共享变量回写到内存时,其他C P U
会通过总线嗅探机制感知该共享变量是否发生变化,如果发生变化,让自己对应的共享变量缓存行失效,重新从内存读取最新的数据,缓存锁定是基于缓存一致性机制来实现的,因为缓存一致性机制会阻止两个以上C P U
同时修改同一个共享变量(现代C P U
基本都支持和使用缓存锁定机制)。(在六、Java内存模型也有详细的介绍)
3.C A S的缺点
C A S
和锁都解决了原子性问题,和锁相比没有阻塞、线程上下文切换、死锁,所以C A S
要比锁拥有更优越的性能,但是C A S
同样存在缺点。
C A S
的问题如下
-
只能保证一个共享变量的原子操作(由cpu缓存一致性机制决定的)
-
自旋时间太长(建立在自旋锁的基础上)
-
ABA问题
3.1只能保证一个共享变量的原子性操作
C A S只能针对一个共享变量使用,如果多个共享变量就只能使用锁了。
3.2自旋时间长
当一个线程获取锁时失败,不进行阻塞挂起,而是间隔一段时间再次尝试获取,直到成功为止,这种循环获取的机制被称为自旋锁(
spinlock
)。
自旋锁好处:持有锁的线程在短时间内释放锁,那些等待竞争锁的线程就不需进入阻塞状态(无需线程上下文切换/无需用户态与内核态切换),它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户态和内核态的切换消耗。
自旋锁缺点:线程在长时间内持有锁,等待竞争锁的线程一直自旋,即CPU一直空转,资源浪费在毫无意义的地方,所以一般会限制自旋次数。
3.3ABA问题
ABA:
C A S需要检查待更新的内存值有没有被修改,如果没有则更新,但是存在这样一种情况,如果一个值原来 是 A,变成了B,然后又变成了A,在C A S检查的时候会发现没有被修改。
假设有两个线程,线程1
读取到内存值A
,线程1
时间片用完,切换到线程2
,线程2
也读取到了内存值A
,并把它修改为B
值,然后再把B
值还原到A
值,简单说,修改次序是A->B->A
,接着线程1
恢复运行,它发现内存值还是A
,然后执行C A S
操作,这就是著名的ABA
问题,但是好像又看不出什么问题。
只是简单的数据结构,确实不会有什么问题,如果是复杂的数据结构可能就会有问题了(使用AtomicReference
可以把C A S
使用在对象上),
以链表数据结构为例,两个线程通过C A S
去删除头节点,假设现在链表有A->B
节点
-
线程
1
删除A
节点,B
节点成为头节点,正要执行C A S(A,A,B)
时,时间片用完,切换到线程2
-
线程
2
删除A、B
节点 -
线程
2
加入C、A
节点,链表节点变成A->C
-
线程
1
重新获取时间片,执行C A S(A,A,B)
-
丢失
C
节点
要解决A B A
问题只要追加版本号即可,每次改变时加1
,即A —> B —> A
,变成1A —> 2B —> 3A
,在Java
中提供了AtomicStampedRdference
可以实现这个方案。
4.Java中提供的J.U.C 并发包。
import java.util.concurrent.atomic
4.1原子整数
public class Test34 {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(5);
/*System.out.println(i.incrementAndGet()); // ++i 1
System.out.println(i.getAndIncrement()); // i++ 2
System.out.println(i.getAndAdd(5)); // 2 , 7
System.out.println(i.addAndGet(5)); // 12, 12*/
// 读取到 设置值
// i.updateAndGet(value -> value * 10);
System.out.println(updateAndGet(i, p -> p / 2));
// i.getAndUpdate()
System.out.println(i.get());
}
public static int updateAndGet(AtomicInteger i, IntUnaryOperator operator) {
while (true) {
int prev = i.get();
int next = operator.applyAsInt(prev);
if (i.compareAndSet(prev, next)) {
return next;
}
}
}
}
4.2原子引用
package cn.itcast.test;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j(topic = "c.Test35")
public class Test35 {
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
}
}
class DecimalAccountCas implements DecimalAccount {
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
// this.balance = balance;
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
while(true) {
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(amount);
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
interface DecimalAccount {
// 获取余额
BigDecimal getBalance();
// 取款
void withdraw(BigDecimal amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
// ts.forEach(t -> {
// try {
// t.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// });
System.out.println(account.getBalance());
}
}
4.2.1 AtomicStampedReference 解决ABA问题
4.3原子数组
package cn.itcast.test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class Test39 {
public static void main(String[] args) {
//不安全的数组
demo(
() -> new int[10],
(array) -> array.length,
(array, index) -> array[index]++,
array -> System.out.println(Arrays.toString(array)));
//安全的数组
demo(
() -> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array));
}
/**
* 参数1,提供数组、可以是线程不安全数组或线程安全数组
* 参数2,获取数组长度的方法
* 参数3,自增方法,回传 array, index
* 参数4,打印数组的方法
*/
<<Supplier、Consumer、BiConsumer是Java8中的函数式接口>>
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer) {
List<Thread> ts = new ArrayList<>();
T array = arraySupplier.get();
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j % length);
}
}));
}
ts.forEach(t -> t.start()); // 启动所有线程
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 等所有线程结束
printConsumer.accept(array);
}
}