注:本笔记是阅读《Java高并发核心编程卷2》整理的笔记!
CAS原理
JUC原子类一Atomic
基本原子类
数组原子类
引用原子类
字段更新原子类
AtomicInteger 线程安全原理
引用类型原子类
属性更新原子类
ABA问题
提升高并发场景下CAS提作的性能
以空间换时间: LongAdder
CAS优势与弊端
提升CAS性能
JVM的synchronized轻量级锁使用CAS(Compare And Swap,比较并交换)进行自旋抢锁, CAS是CPU指令级的原子操作并处于用户态下,所以JVM轻量级锁开销较小。 在java.util.concurrent.atomic包的原子类(如AtomicXXX中)都使用了CAS保障对数字成员进行操作的原子性。java.util.concurrent的大多数类(包括显式锁、并发容器)都是基于AQS和AtomicXXX实现的,其中AQS通过CAS保障其内部双向队列队头、队尾操作的原子性
CAS原理
JDK 5所增加的JUC(java.util.concurrent)并发包对操作系统的底层CAS原子操作进行了封装,为上层Java程序提供了CAS操作的API。 CAS是一种无锁算法,该算法关键依赖两个值——期望值(就值)和新值,底层CPU利用原子操作判断内存原值与期望值是否相等,如果相等就给内存地址赋新值,否则不做任何操作。
使用CAS进行无锁编程的步骤大致如下:
- 获得字段的期望值(oldValue)。
- 计算出需要替换的新值(newValue)。
- 通过CAS将新值(newValue)放在字段的内存地址上,如果CAS失败就重复第1)步到第2)步,直到CAS成功,这种重复俗称CAS自旋。
do
{获得字段的期望值(expValue),也就是读取内存原值;计算出需要替换的新值(newValue);
} while (!CAS(内存地址, expValue, newValue)) // 判断期望值是否等于现在的内存原值,如果等于表示此期间没人修改,否则就被修改了,重新获得期望值。
假设共享变量V内存值为100,线程A对V减1操作,线程B对V加1操作。使用CAS实现:无论A、B想要对其修改都先获取V的值,作为期望值,然后才进行操作,最后提交修改的时候需要用开始的期望值去与内存原值V进行对比,如果相等表示未被修改。如果不相等表示已被修改了,重新进行前面操作,获取V值作为期望值。。。
当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少, CAS性能会很高;当并发修改的线程多,冲突出现的机会多时,自旋的次数也会很多, CAS性能会大大降低。所以,提升CAS无锁编程效率的关键在于减少冲突的机会。 所以CAS适合用于并发线程数较少的场景。
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果
可以使用Unsafe类实现CAS原子操作:
//通过CAS原子操作,进行“比较并交换”
public final boolean unSafeCompareAndSet(int oldValue, int newValue)
{ //valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据。原子操作:使用unsafe的“比较并交换”方法进行value属性的交换return unsafe.compareAndSwapInt( this, valueOffset,oldValue ,newValue );
}
JUC原子类—Atomic
在多线程并发执行时,诸如“++”或“–”类的运算不具备原子性,不是线程安全的操作。通常情况下,大家会使用synchronized将这些线程不安全的操作变成同步操作,但是这样会降低并发程序的性能。所以, JDK为这些类型不安全的操作提供了一些原子类,与synchronized同步机制相比, JDK原子类基于CAS轻量级原子操作实现,使得程序运行效率变得更高。
JUC并发包中原子类都存放在java.util.concurrent. atomic类路径下,可以将JUC包中的原子类分为4类:基本原子类、数组原子类、原子引用类和字段更新原子类。 主要使用的为基本原子类和数组原子类,其他稍作了解。
基本原子类
基本原子类的功能是通过原子方式更新Java基础类型变量的值 :
- AtomicInteger:整型原子类。
- AtomicLong:长整型原子类。
- AtomicBoolean:布尔型原子类。
在多线程环境下,如果涉及基本数据类型的并发操作,不建议采用synchronized重量级锁进行线程同步,而是建议优先使用基础原子类保障并发操作的线程安全性。 基础原子类AtomicInteger常用的方法主要如下:
public final int get() //获取当前的值
public final int getAndSet(int newValue) //获取当前的值,然后设置新的值
public final int getAndIncrement() //获取当前的值,然后自增
public final int getAndDecrement() //获取当前的值,然后自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update); //通过CAS方式设置整数值int tempvalue = 0;
AtomicInteger i = new AtomicInteger(0);
//取值,然后设置一个新值 i=3
tempvalue = i.getAndSet(3);
//取值,然后自增 i=4
tempvalue = i.getAndIncrement();
//取值,然后增加5 i=9
tempvalue = i.getAndAdd(5);
//CAS交换 i=100
boolean flag = i.compareAndSet(9, 100);
数组原子类
数组原子类的功能是通过原子方式更新数组中的某个元素的值 :
- AtomicIntegerArray:整型数组原子类。
- AtomicLongArray:长整型数组原子类。
- AtomicReferenceArray:引用类型数组原子类
上面三个类提供的方法几乎相同,所以我们这里以AtomicIntegerArray为例来介绍。AtomicIntegerArray类常用方法如下:
//获取 index=i 位置元素的值
public final int get(int i)
//返回index=i位置的当前的值,并将其设置为新值: newValue
public final int getAndSet(int i, int newValue)
//获取index=i位置元素的值,并让该位置的元素自增
public final int getAndIncrement(int i)
//获取index=i位置元素的值,并让该位置的元素自减
public final int getAndDecrement(int i)
//获取index=i位置元素的值,并加上预期的值
public final int getAndAdd(int delta)
//如果输入的数值等于预期值,就以原子方式将位置i的元素值设置为输入值(update)
boolean compareAndSet(int expect, int update)
//最终将位置i的元素设置为newValue
//lazySet方法可能导致其他线程在之后的一小段时间内还是可以读到旧的值
public final void lazySet(int i, int newValue);int tempvalue = 0;
//原始的数组
int[] array = { 1, 2, 3, 4, 5, 6 };
//包装为原子数组
AtomicIntegerArray i = new AtomicIntegerArray(array);
//获取第0个元素,然后设置为2 ,输出 tempvalue:1; i:[2, 2, 3, 4, 5, 6]
tempvalue = i.getAndSet(0, 2);
//获取第0个元素,然后自增,输出tempvalue:2; i:[3, 2, 3, 4, 5, 6]
tempvalue = i.getAndIncrement(0);
//获取第0个元素,然后增加一个delta 5,输出tempvalue:3; i:[8, 2, 3, 4, 5, 6]
tempvalue = i.getAndAdd(0, 5);
引用原子类
引用原子类主要包括以下三个:
- AtomicReference:引用类型原子类。
- AtomicMarkableReference:带有更新标记位的原子引用类型。
- AtomicStampedReference:带有更新版本号的原子引用类型。
字段更新原子类
字段更新原子类主要包括以下三个:
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器。
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicInteger 线程安全原理
基础原子类(以AtomicInteger为例)主要通过CAS自旋+volatile相结合的方案实现,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的高开销,使得Java程序的执行效率大为提升。 CAS用于保障变量操作的原子性, volatile关键字用于保障变量的可见性(即一个线程修改了某个volatile变量的值,该值对其他线程立即可见。),二者常常结合使用。
源代码实现:
//Unsafe类实例,也是使用Unsafe类的CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
//内部value值,使用volatile保证线程可见性
private volatile int value;
//对比expect(期望值)与value,若不同则返回false
//若expect与value相同,则将新值赋给value,并返回true,否则循环自旋,直到成功
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
引用类型原子类
基础的原子类型只能保证一个变量的原子操作,当需要对多个变量进行操作时, CAS无法保证原子性操作,这时可以用AtomicReference(原子引用类型)保证对象引用的原子性。简单来说,如果需要同时保障对多个变量操作的原子性,就可以把多个变量放在一个对象中进行操作。
使用原子引用类型AtomicReference包装了User对象之后,只能保障User引用的原子操作,对被包装的User对象的字段值修改时不能保证原子性,这点要切记。
这里以AtomicReference为例子来介绍 ,首先定义一个User类,属性包括 uid,nickName,age三个。
public class User implements Serializable{String uid; //用户IDString nickName; //昵称public volatile int age; //年龄public User(String uid, String nickName){this.uid = uid;this.nickName = nickName;}@Overridepublic String toString(){return "User{" +"uid='" + getUid() + '\'' +", nickName='" + getNickName() + '\'' +", platform=" + getPlatform() +'}';}
}
使用AtomicReference对User的引用进行原子性修改,代码如下:
//包装的原子对象
AtomicReference<User> userRef = new AtomicReference<User>();
//待包装的User对象
User user = new User("1", "张三");
//为原子对象设置值
userRef.set(user);
//要使用CAS替换的User对象
User updateUser = new User("2", "李四");
//使用CAS替换 , 成功success为true,user为李四
boolean success = userRef.compareAndSet(user, updateUser);
属性更新原子类
如果需要保障对象某个字段(或者属性)更新操作的原子性,需要用到属性更新原子类。这里以AtomicIntegerFieldUpdater为例来介绍,使用属性更新原子类保障属性安全更新的流程大致需要两步:
- 第一步,更新的对象属性必须使用public volatile修饰符。
- 第二步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设臵想要更新的类和属性。
//使用静态方法newUpdater()创建一个更新器updater
AtomicIntegerFieldUpdater<User> updater=
AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
User user = new User("1", "张三");
//使用属性更新器的getAndIncrement、 getAndAdd增加user的age值
Print.tco(updater.getAndIncrement(user)); // 1
Print.tco(updater.getAndAdd(user, 100)); // 101
//使用属性更新器的get获取user的age值
Print.tco(updater.get(user)); // 101
ABA问题
比如一个线程A从内存位置M中取出V1,另一个线程B也取出V1。现在假设线程B进行了一些操作之后将M位置的数据V1变成了V2,然后又在一些操作之后将V2变成V1。之后,线程A进行CAS操作,但是线程A发现M位置的数据仍然是V1,最后线程A操作成功。尽管线程A的CAS操作成功,但是不代表这个过程是没有问题的,线程A操作的数据V1可能已经不是之前的V1,而是被线程B替换过的V1,这就是ABA问题。
举例:假设共享变量V内存值为100,线程A对V减1再加1操作,线程B对V加1再减1操作。使用CAS实现:无论A、B想要对其修改都先获取V的值,作为期望值,然后才进行操作,最后提交修改的时候需要用开始的期望值去与内存原值V进行对比,如果相等表示未被修改。如果不相等表示已被修改了。在这种情况下,无论A、B线程谁先读取并修改,之后的线程总能不通过自旋修改成功!即好像这个乐观锁不存在似的!
解决方案:
使用乐观锁的版本号方式,乐观锁每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。
JDK提供了一个类似AtomicStampedReference类来解决ABA问题。AtomicStampReference在CAS的基础上增加了一个Stamp(印戳或标记),使用这个印戳可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。 AtomicStampReference的compareAndSet()方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳标志是否等于预期标志,如果全部相等,就以原子方式将引用值和印戳标志的值更新为给定的更新值。
//compareAndSet方法的第一个参数是原CAS中的原参数,第二个参数是要替换后的新参数,第
//三个参数是原来CAS数据旧的版本号,第四个参数表示替换后的版本号。
public boolean compareAndSet(V expectedReference, //预期引用值V newReference, //更新后的引用值int expectedStamp, //旧的版本号int newStamp) //新的版本号,+1即可
提升高并发场景下 CAS 操作的性能
在争用激烈的场景下,会导致大量的CAS空自旋。比如,在大量的线程同时并发修改一个AtomicInteger时,可能有很多线程会不停地自旋,甚至有的线程会进入一个无限重复的循环中。大量的CAS空自旋会浪费大量的CPU资源,大大降低了程序的性能。大量的CAS操作还可能导致“总线风暴” 。在高并发场景下如何提升CAS操作性能呢?可以使用LongAdder替代AtomicInteger。
以空间换时间: LongAdder
Java 8提供一个新的类LongAdder,以空间换时间的方式提升高并发场景下CAS操作性能。LongAdder核心思想就是热点分离,与ConcurrentHashMap的设计思想类似:将value值分离成一个数组,当多线程访问时,通过Hash算法将线程映射到数组的一个元素进行操作;而获取最终的value结果时,则将数组的元素求和。LongAdder的内部成员包含一个base值和一个cells数组。在最初无竞争时,只操作base的值;当线程执行CAS失败后, 才初始cells数组,并为线程分配所对应的元素。 相当于分段乐观锁!
//下面这种是传统做法
//定义一个原子对象
AtomicLong atomicLong = new AtomicLong(0);
atomicLong.incrementAndGet();
sout(atomicLong.get());
//下面这种是LongAdder做法
//定义一个LongAdder 对象
LongAdder longAdder = new LongAdder();
longAdder.add(1);
sout(longAdder.longValue());
AtomicLong使用内部变量value保存着实际的long值,所有的操作都是针对该value变量进行。也就是说,在高并发环境下, value变量其实是一个热点,也就是N个线程竞争一个热点。重试线程越多,就意味着CAS的失败概率越高,从而进入恶性CAS空自旋状态。 LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽(元素)中,各个线程只对自己槽中的那个值进行CAS操作。这样热点就被分散了,冲突的概率就小很多。 使用LongAdder,即使线程数再多也不担心,各个线程会分配到多个元素上去更新,增加元素个数就可以降低 value的“热度”, AtomicLong中的恶性CAS空自旋就解决了。如果要获得完整的LongAdder存储的值,只要将各个槽中的变量值累加,返回最终的累加之后的值即可。LongAdder的实现思路与ConcurrentHashMap中分段锁基本原理非常相似,本质上都是不同的线程在不同的单元上进行操作,这样减少了线程竞争,提高了并发效率。
CAS优势与弊端:
CAS的优势主要有两点:
- 属于无锁编程,线程不存在阻塞和唤醒这些重量级的操作。
- 进程不存在用户态和内核态之间的运行切换,进程不需要承担频繁切换的开销。
CAS的弊端:
- ABA问题。解决方法:版本号机制。JDK提供了AtomicStampedReference 版本号解决ABA问题。印戳 作为版本。
- 只能保证一个共享变量之间的原子性操作 ,规避方法为:把多个共享变量合并成一个共享变量来操作。 规避方法合并成一个对象,JDK提供了AtomicReference类来保证引用对象之间的原子性 。
- 无效CAS会带来开销问题,自旋CAS如果长时间不成功(不成功就一直循环执行,直到成功为止),就会给CPU带来非常大的执行开销。
提升CAS性能
- 分散操作热点,使用LongAdder替代基础原子类AtomicLong, LongAdder将单个CAS热点(value值)分散到一个cells数组中。
- 使用队列削峰,将发生CAS争用的线程加入一个队列中排队,降低CAS争用的激烈程度。自旋改成队列排队。