1、C A S基本概念
C A S(compareAndSwap)也叫比较交换,是一种无锁原子算法,映射到操作系统就是一条cmpxchg硬件汇编指令(保证原子性),其作用是让C P U将内存值更新为新值,但是有个条件,内存值必须与期望值相同,并且C A S操作无需用户态与内核态切换,直接在用户态对内存进行读写操作(意味着不会阻塞/线程上下文切换)。
它包含3个参数C A S(V,E,N),V表示待更新的内存值,E表示预期值,N表示新值,当 V值等于E值时,才会将V值更新成N值,如果V值和E值不等,不做更新,这就是一次C A S的操作。
简单说,C A S需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的,如果变量不是你想象的那样,说明它已经被别人修改过了,你只需要重新读取,设置新期望值,再次尝试修改就好了。
2、C A S源码分析
2.1 Unsafe类
原子类中的主要组成部分:
1、Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都是直接调用操作系统底层资源执行相应任务。
2、变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3、变量value用volatile修饰,保证了多线程之间的内存可见性。
例如 AtomicInteger 类调用incrementAndGet()方法实现原子性的自增,内部调用Unsafe的getAndAddInt方法:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
在Unsafe类的getAndAddInt方法中主要是看compareAndSwapInt方法:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
可以看到Unsafe类中的compareAndSwapInt是一个native本地方法:
public final native boolean compareAndSwapInt(Object var1, long var2,
int var4, int var5);
- var1:表示要操作的对象
- var2:表示要操作对象中属性地址的偏移量
- var4:表示需要修改数据的期望的值
- var5:表示需要修改为的新值
不难发现,cmpxchg这条汇编语言可以直接操作内存进行数据交换,实现CAS最终目的。(一条汇编指令对应一条CPU指令,是单步操作,自然是原子性的,因此谁CAS实现是硬件层面上的)
这里看到有一个LOCK_IF_MP,作用是如果是多处理器,在指令前加上LOCK前缀,因为在单处理器中,是不会存在缓存不一致的问题的,所有线程都在一个CPU上跑,使用同一个缓存区,也就不存在缓存与主内存不一致的问题,不会造成可见性问题。
(缓存在CPU上,主内存不在CPU上,CPU是通过缓存去读取主内存的,每个CPU对应一个缓存,不同缓存对应不同CPU,这里要结合前面的JMM模型和硬件架构理一理)
然而在多核处理器中,需要遵循缓存一致性协议通知其他处理器更新自己的缓存。
Lock在这里的作用:
- 在cmpxchg执行期间,锁住内存地址[edx],其他处理器不能访问该内存,保证原子性。(这个就是保证CAS原子性的关键所在)
- 写内存屏障,保证每个线程的本地空间与主存一致。
- 禁止cmpxchg与前后任何指令重排序,防止指令重排序。
以使用AtomicInteger对变量进行自增操作为例,可以得到如下主要流程:
假设线程A和现场B两个线程同时执行 getAndIncrement()方法(分别跑在不同CPU上):
- 假设主内存中 value原始值为3,根据JMM模型,线程A 和 线程B各自持有一份值为3的value的副本分别到各自的工作内存。
- 线程A通过getIntVolatile(var1, var2)拿到value值3,假设这时线程A被挂起。
- 线程B也通过getIntVolatile(var1, var2)拿到value值3,此时线程B没有被挂起并执行 compareAndSwapInt 方法,比较内存值也为3,则成功修改内存值为4,线程B执行完毕。
- 此时线程A被唤醒,执行compareAndSwapInt 方法比较,发现主内存中的值和旧的预期值不一致,说明该值已经被其他线程更新了,则线程A本次修改失败,自旋重来一次。
- 线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A是可见的,线程A继续执行 compareAndSwapInt 进行比较替换,直到成功为止。
2.2 AtomicReference<V>
AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。
AtomicReference是作用是对”对象”进行原子操作。 提供了一种读和写都是原子性的对象引用变量。原子意味着多个线程试图改变同一个AtomicReference(例如比较和交换操作)将不会使得AtomicReference处于不一致的状态。
AtomicReference<User> userAtomicReference=new AtomicReference<>();
User user=new User("张三","24");
userAtomicReference.set(user);
User user2=new User("张三","25");
Runnable runnable = new Runnable() {
@Override
public void run() {
userAtomicReference.compareAndSet(user, user2);
System.out.println("runnable1: "+userAtomicReference.get());
}
};
Runnable runnable2 = new Runnable() {
@Override
public void run() {
user2.setAge("23");
User user3=new User("张三","27");
System.out.println(userAtomicReference.get()==user2);
userAtomicReference.compareAndSet(user2, user3);
System.out.println("runnable2: "+userAtomicReference.get());
}
};
runnable.run();
runnable2.run();
System.out.println("main: "+userAtomicReference.get());
由输出结果可知,AtomicReference可以保证对象的原子性。其中,判断对象是否一致,比较的是对象的地址,而非属性值。
3、C A S的问题
C A S和锁都解决了原子性问题,和锁相比没有阻塞、线程上下文你切换、死锁,所以C A S要比锁拥有更优越的性能,但是C A S同样存在缺点。C A S的问题如下:
- 只能保证一个共享变量的原子操作
- 自旋时间太长(建立在自旋锁的基础上)
- ABA问题
3.1 只能保证一个共享变量的原子操作
看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。
例如:JDK提供的AtomicReference类来保证对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
3.2 自旋时间太长(建立在自旋锁的基础上)
当一个线程获取锁时失败,不进行阻塞挂起,而是间隔一段时间再次尝试获取,直到成功为止,这种循环获取的机制被称为自旋锁(spinlock)。
自旋锁好处是,持有锁的线程在短时间内释放锁,那些等待竞争锁的线程就不需进入阻塞状态(无需线程上下文切换/无需用户态与内核态切换),它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户态和内核态的切换消耗。
自旋锁坏处显而易见,线程在长时间内持有锁,等待竞争锁的线程一直自旋,即CPU一直空转,资源浪费在毫无意义的地方,所以一般会限制自旋次数。
最后来说自旋锁的实现,实现自旋锁可以基于C A S实现,先定义lockValue对象默认值1,1代表锁资源空闲,0代表锁资源被占用,代码如下:
上面定义了AtomicInteger类型的lockValue变量,AtomicInteger是Java基于C A S实现的Integer原子操作类,还定义了3个函数lock、tryLock、unLock。
(1)tryLock函数-获取锁
- 期望值1,更新值0。C A S更新如果期望值与lockValue值相等,则lockValue值更新为0,返回true,否则执行下面逻辑如果期望值与lockValue值不相等,不做任何更新,返回false。
- 期望值0,更新值1。C A S更新如果期望值与lockValue值相等,则lockValue值更新为1,返回true,否则执行下面逻辑如果期望值与lockValue值不相等,不做任何更新,返回false。
(2)lock函数-自旋获取锁
(3)unLock函数-释放锁
从上图可以看出,只有tryLock成功的线程(把lockValue更新为0),才会执行代码块,其他线程个tryLock自旋等待lockValue被更新成1,tryLock成功的线程执行unLock(把lockValue更新为1),自旋的线程才会tryLock成功。
3.3 ABA问题
3.3.1 什么是ABA问题:
- 线程1和线程2开启时,根据对线程变量的操作,把主内存的值A复制到线程中的工作内存A
- 线程1需要10s,线程2需要2s,假设线程2先修改则线程2中的工作内存的值A和主内存中的值A修改为B
- 等待线程1的过程中,线程2又把自己内存中的值和主内存中的值修改为“A”
- 此时线程1开启,发现线程1中的A与主内存中的“A”相同,按照CAS的方法把值修改为B
简单的来说就是由于线程1和线程2存在时间差,线程2执行完之后又执行了一次改回来”原来的“值,线程1认为和自己的值相同,则又进行了操作。
如果cas的是个简单的数据结构,则基本上不存在问题,如果是复杂的结构,则会出现问题。列入:
LinkedList<Integer> list = new LinkedList<>();
list.add(2);
list.add(3);
AtomicReference<LinkedList<Integer>> linkedListAtomicReference=new AtomicReference<>(list);
new Thread(()->{
System.out.println("start runnable1: "+linkedListAtomicReference.get());
LinkedList<Integer> linkedList = linkedListAtomicReference.get();
linkedList.removeFirst();
try {
Thread. sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
linkedList.add(4);
linkedListAtomicReference.compareAndSet(linkedList,linkedList);
System.out.println("end runnable1: "+linkedListAtomicReference.get());
}).start();
new Thread(()->{
System.out.println("start runnable2: "+linkedListAtomicReference.get());
LinkedList<Integer> linkedList = linkedListAtomicReference.get();
linkedList.removeFirst();
linkedList.add(5);
linkedListAtomicReference.compareAndSet(linkedList,linkedList);
System.out.println("end runnable2: "+linkedListAtomicReference.get());
}).start();
try {
Thread.sleep(5000);
System.out.println(linkedListAtomicReference.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
list为2—>3
runnable1需求:去掉头,新增节点4。
runnable2需求:去掉头,新增节点5。
执行流程:
runnable1去掉头以后,睡眠。此时list为3。
runnable2拿到执行权,去掉头以后,加5,runnable2执行结束。此时list为5。
runnable拿到执行权,新增节点4。此时list为为5,4。
由于AtomicReference比较的是对象地址,
runnable2操作完以后,runnable1进行cas时,判断list相等,其实内容已经改变。
3.3.2 解决ABA问题-AtomicStampedReference版本号原子引用
除去比较对象的值以外,在新增一个版本号。只有当值与版本号一致时,才认为与气质与实际值相等。
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
new Thread(()->{
int stamped = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"初始版本号为"+stamped);
try{
TimeUnit.SECONDS.sleep(1);
System.out.println(atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));
System.out.println("第一次修改后版本号为"+atomicStampedReference.getStamp());
System.out.println("第一次修改后当前值"+atomicStampedReference.getReference());
System.out.println(atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));
System.out.println("第二次修改后版本号为"+atomicStampedReference.getStamp());
System.out.println("第一次修改后当前值"+atomicStampedReference.getReference());
}catch(InterruptedException e){
e.printStackTrace();
}
}).start();
new Thread(()->{
int stamped = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"初始版本号为"+stamped);
try{
TimeUnit.SECONDS.sleep(2);
System.out.println(atomicStampedReference.compareAndSet(100,2022,stamped,stamped+1));
}catch(InterruptedException e){
e.printStackTrace();
}
}).start();
- 线程1和线程2获取的值都为:版本号1,值100
- 线程1将值修改为101,版本为2。又将值修改回为100 ,版本值为3.
- 线程2设置值为2022时,判断预期值与内存值都为100,但是预期的版本号1和现在版本号3不等,cas失败。防止了ABA问题的发生。