Java并发之CAS原理学习篇
大厂常见面试题:
- 你了解CAS吗,请谈谈你对CAS的理解?
- CAS 底层原理是如何实现的?
- 谈谈对 UnSafe 的理解?
- CAS 的缺点?
- 原子类 AtomicInteger 的 ABA 问题谈一谈?原子更新引用知道吗?
1.你了解CAS吗,请谈谈你对CAS的理解?
CAS全称:“Compare And Swap”,意思是比较并交换。对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问时没有冲突的,既然没有冲突就不需要等待,线程不需要阻塞。那多个线程共同访问临界区的资源怎么办呢,无锁的策略采用一种比较交换技术来鉴别线程冲突,一旦检测到冲突,就充实当前操作指导没有冲突为止。
举个例子:
package com.jmm.volatiles;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 如何解决原子性
* 1、加synchronized
* 2、使用juc下的AtomicInteger(也就是今天所学的CAS操作)
**/
class Date {
//验证保证原子性方法
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement(); //每次增加1
}
}
public class VolatileDemo {
public static void main(String[] args) {
Date date = new Date();
for (int i = 0; i < 20; i++) {
new Thread(()->{ //Lambda表达式创建线程方式,java8新特性
for (int j = 1; j <= 1000; j++) {
date.addAtomic(); //保证原子性
}
},String.valueOf(i)).start();
}
// 默认有 main 线程和 gc 线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
//输出值
System.out.println(Thread.currentThread().getName() + '\t' + date.atomicInteger); //输出20000
}
}
getAndIncrement();
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
- 分析一下 getAndAddInt 这个方法
// unsafe.getAndAddInt
public final int getAndAddInt(Object obj, long valueOffset, long expected, int val) {
int temp;
do {
temp = this.getIntVolatile(obj, valueOffset); // 获取快照值
} while (!this.compareAndSwap(obj, valueOffset, temp, temp + val)); // 如果此时 temp 没有被修改,就能退出循环,否则重新获取
return temp;
}
2.CAS 底层原理是如何实现的?
CAS 并发原体现在 JAVA 语言中就是 sun.misc.Unsafe 类中的各个方法。调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖硬件的功能,通过它实现了原子操作。由于 CAS 是一种系统源语,源语属于操作系统用语范畴,是由若干条指令组成,用于完成某一个功能的过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说 CAS 是一条原子指令,不会造成所谓的数据不一致的问题。
它的功能是判断内存某一个位置的值是否为预期,如果是则更改这个值,这个过程就是原子的。
3.谈谈对 UnSafe 的理解?
Unsafe中对CAS的实现是C++写的,从上图可以看出最后调用的是Atomic:comxchg这个方法,这个方法的实现放在hotspot下的os_cpu包中,说明这个方法的实现和操作系统、CPU都有关系
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 获取下面 value 的地址偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
// ...
省略其它源码
}
- Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,而需要通过本地(native)方法来访问, Unsafe 类相当一个后门,基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中,其内部方法操作可以像 C 指针一样直接操作内存,因为 Java 中 CAS 操作执行依赖于 Unsafe 类。
- 变量 vauleOffset,表示该变量值在内存中的偏移量,因为 Unsafe 就是根据内存偏移量来获取数据的。
- 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性。
4.CAS 的缺点?
- 循环时间长开销很大:
我们可以看到前面例子的getAndInt方法执行时,有个do while,如果 CAS 失败,会一直尝试,如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销(比如线程数很多,每次比较都是失败,就会一直循环),所以希望是线程数比较小的场景。
- 只能保证一个共享变量的原子操作
- 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性。
- 引出 ABA 问题
5.ABA问题
CAS算法实现的一个重要前提需要提取内存中某时刻的数据并在当下时刻比较替换,那么在这个时间差类会导致数据的变化。比如一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A ,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程是没有问题的。
ABA问题的解决办法:
1.在变量前面追加版本号:每次变量更新就把版本号加1,则A-B-A就变成1A-2B-3A。
2.atomic包下的AtomicStampedReference类:其compareAndSet方法首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用的该标志的值设置为给定的更新值。
原子更新引用
class User {
private String name;
private int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User lisi= new User("lisi", 18);
User zhangsan= new User("zhangsan", 20);
//原子引用
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(lisi);
System.out.println(atomicReference.compareAndSet(lisi, zhangsan)); // 输出true
System.out.println(atomicReference.get()); // 输出User(userName=zhangsan, age=20)
}
}
ABA问题的产生,及解决方法?(使用版本号记录,也就是时间戳概念)
先看结果,在分析代码:
package com.cas.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @description: ABA问题解决方法:版本号(时间戳)
* @author: Mr.Li
* @create: 2019-09-22 17:23
**/
public class ABADemo {
//产生ABA问题
private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
//加了版本号,解决ABA问题
private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
public static void main(String[] args) {
System.out.println("=======以下是ABA问题的产生======");
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean t = atomicReference.compareAndSet(100,520);
System.out.println("是否修改成功:" + t + "\t当前值:" + atomicReference.get());
},"t2").start();
//暂停一会,让上面的例子先结束
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("=======以下是ABA问题的解决======");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
//暂停1秒,让t4过得第一次版本号
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t第2次版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
//暂停3秒,等待t3线程完成ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = atomicStampedReference.compareAndSet(100, 520, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t是否修改成功:" + b + "\t当前版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t当前实际最新值:" + atomicStampedReference.getReference()); // 100
},"t4").start();
}
}
每日一言:
忠诚于自己的选择,不为任何人证明什么,只为了给自己一个交代