十二、Java高级特性(CAS基本原理)
一、什么是原子操作?如何实现原子操作?
如果有两个操作A和B,分别有两个不同的线程执行。从A所在线程来看,执行B的线程执行B的时候要么将B执行完毕,要么完全不执行B,那么A和B对彼此来说是原子的。
1、实现原子操作可以用锁
锁机制基本满足了需求是没有问题的。但是有时候我们需要更加有效,更加灵活,synchronized关键字是基于阻塞的锁机制。也就是说当有一个线程拥有锁的时候,访问同一个资源的其他线程必须处于等待状态。直到该线程释放锁。
这就会有一个问题:假如被阻塞的线程优先级很高怎么办?其次就是获得锁的线程一直不释放锁怎么办?还有就是如果有大量的线程来竞争资源,那么CPU要花大量的时间和资源来处理这些竞争。同时还可能出现死锁的情况。锁是一种比较粗糙,粒度比较大的机制,对于一些像计数器这样的需求,就显得比较笨重。
2、实现原子操作还可以使用现代CPU支持的CAS指令
每一个CAS包含了三个运算符:
(1)内存地址V
(2)期望值A
(3)新值B
CAS举例
现在我们有3个线程要对一个共享变量i实现+1的操作,理论上三个线程+1完最后的结果是3。我们可以使用synchronized锁的机制来实现,同时也可以使用更加轻量的CAS指令实现。
下面我们介绍其原理:如下图

(1)三个线程都是都可以进行i++,不需要阻塞等待,但是+1的时候并没有真正的写入内存。每个线程i++完之后,各自的i都是1。
(2)进入图中的compare swap操作,比较和交换,这个操作属于原子操作。即要么全部执行,要么不执行,因此在这每次只有一个线程执行或者不执行。假如三个线程分别为A 、B、C。A线程进入原子操作比较和交换的时候,i的内存地址是V,期望值是i= 0,新值是i= 1。如果比较的时候i确实等于0,那么进行交换,将i = 1,写入内存,A线程原子操作完毕。B线程也一样,i的内存地址是V,期望值是i= 0,新值是i= 1。此时B线程进行原子操作比较交换的时候发现i= 1,自己的期望值是0。他们俩不相等,因此从新来一次,将期望值置为1,新值i= 1+1 =2。在进行原子操作比较和交换,这个时候如果C线程没有经过原子操作修改过i的值,那么此时B线程原子操作比较交换的时候,内存地址V中i的值是1,B的期望值是1,新值是2。内存V中地址的值和期望值一样,因此交换。把新值i= 2写入内存地址V中。当C线程进来的是也是一样的道理。在原子操作的时候,当原子操作比较交换的时候,内存V中的值和期望值不一样,那么再来一次,将期望值修改。这个操作称为自旋。
二、实现原子操作的三大问题
1、ABA问题
因为CAS需要在操作值的时候,检查内存V中的值和自己的期望值是否相等,如果相等,则将新值写入内存。但是并不知道内存V中的值中间是否发生了变化,例如当一个线程的期望值是A,但是内存地址V中的值,经过其他线程从A改成B,从B又改成A。当该线程进行CAS原子操作的时候,发现内存地址V中的值是A,和自己的期望值相等,那么就直接将新值写入到内存。然而它并不知道其实值已经经过多次的变化。
那么如果对于需要知道A是否被更改过的要求,就会产生问题。解决这一问题,可以加一个版本号。也就是每次更改内存V中的值的时候,把版本号+1。通过拿到版本号就可以知道是否被更改过。
2、循环时间过长,开销大。
自旋CAS如果时间太长,会给CPU带来大的执行开销。
3、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作
三、Jdk中相关原子操作类的使用
- int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
- boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
- int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
- int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
- AtomicIntegerArray
主要是提供原子的方式更新数组里的整型,其常用方法如下。 - int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
- boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。 - 更新引用类型
原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。 - AtomicReference
- 原子更新引用类型。
- AtomicStampedReference
利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark。 还是那个水的例子,AtomicStampedReference可能关心的是动过几次,AtomicMarkableReference关心的是有没有被人动过,方法都比较简单。 - AtomicMarkableReference:
原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。
四、AtomicInteger的使用举例
package com.it.test.thread.consumer_product.cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 基本类型的原子操作
*/
public class CasTest {
static AtomicInteger atomicInteger = new AtomicInteger(10);
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
System.out.println(atomicInteger.getAndIncrement());//i++
}
}.start();
new Thread(){
@Override
public void run() {
System.out.println(atomicInteger.incrementAndGet());//++i
}
}.start();
new Thread(){
@Override
public void run() {
System.out.println( atomicInteger.getAndAdd(24));//i++
}
}.start();
new Thread(){
@Override
public void run() {
System.out.println(atomicInteger.addAndGet(30));//++i
}
}.start();
}
}
10
12
12
66