目录
JUC 并发包中原子类 java.util.concurrent.atomic
根据操作的目标数据类型,可以将 JUC 包中的原子类分为 4 类:
JUC 并发包中原子类 java.util.concurrent.atomic
- volatile解决多线程内存不可见问题对于一些多读,是可以解决变量同步问题,但是如果是多谢,同样无法解决线程安全问题
根据操作的目标数据类型,可以将 JUC 包中的原子类分为 4 类:
- 基本原子类
- 数组原子类
- 原子引用类型
- 字段更新原子类
- 原子操作增强类深度剖析
1. 基本原子类
- 主要利用CAS+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升.
- 基本原子类的功能,是通过原子方式更新 Java 基础类型变量的值。基本原子类主要包括了以下三个
- AtomicInteger:整型原子类。
- AtomicLong:长整型原子类。
- AtomicBoolean :布尔型原子类。
2. 数组原子类
- 数组原子类的功能,是通过原子方式更数组里的某个元素的值。数组原子类主要包括了以下三个:
- AtomicIntegerArray:整型数组原子类。
- AtomicLongArray:长整型数组原子类。
- AtomicReferenceArray :引用类型数组原子类。
3. 引用原子类
- 引用原子类主要包括了以下三个:
- AtomicReference:引用类型原子类。
- AtomicStampedReference:带有更新版本号的原子引用类型:通过引入“版本”的概念,来解决ABA的问题。
- 携带版本号的引用类型源自类,可以解决ABA问题
- 解决修改过几次
- 状态戳源自引用,Demo见下面
- AtomicMarkableReference
- 原子更新带有标记位的引用类型对象
- 解决是否修改过:它的定义就是将状态戳简化为true|false,类似于一次性筷子
4. 字段更新原子类
- 字段更新原子类主要包括了以下三个:
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- 可以对指定类的volatile int字段进行更新
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
class BankAccount{ String bankName = "CCB"; //更新的对象属性必须使用public volatile修饰 public volatile int moneny = 0; //synchronized,保证高性能原子性 public synchronized void addMoney(){ moneny++; } //因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 // newUpdater()创建一个更新器,并且需要设置想要更新的类和属性 AtomicIntegerFieldUpdater<BankAccount> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "moneny"); //不加synchronized,保证高性能原子性,局部微创小手术 public void transMoney(BankAccount bankAccount){ fieldUpdater.getAndIncrement(bankAccount); } } public class AtomicIntegerFieldUpdaterDemo { public static void main(String[] args) { BankAccount bankAccount = new BankAccount(); CountDownLatch count = new CountDownLatch(10); for (int i = 0; i < 10; i++) { new Thread( () -> { try { for (int i1 = 0; i1 < 1000; i1++) { bankAccount.transMoney(bankAccount); } }finally { count.countDown(); } }).start(); } try { count.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("result:======>" + bankAccount.moneny); } }
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- 可以对指定类的volatile Long字段进行更新
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段
- 可以对指定类的volatile 引用字段进行更新
class BankAccount{ //更新的对象属性必须使用public volatile修饰 public volatile Boolean isInit = Boolean.FALSE; //因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 // newUpdater()创建一个更新器,并且需要设置想要更新的类和属性 AtomicReferenceFieldUpdater<BankAccount, Boolean> fieldUpdater = AtomicReferenceFieldUpdater.newUpdater(BankAccount.class, Boolean.class, "isInit"); //不加synchronized,保证高性能原子性,局部微创小手术 public void init(BankAccount bankAccount){ if (fieldUpdater.compareAndSet(bankAccount, Boolean.FALSE, Boolean.TRUE)) { System.out.println(Thread.currentThread().getName() + "\t" + "=====start init....."); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "=====end init....."); }else { System.out.println(Thread.currentThread().getName() + "\t" + "had been init....."); } } } public class AtomicIntegerFieldUpdaterDemo { public static void main(String[] args) { BankAccount bankAccount = new BankAccount(); for (int i = 0; i < 10; i++) { new Thread( () -> { bankAccount.init(bankAccount); }).start(); } } }
- 使用目的
- 以一种线程安全的方式操作非线程安全对象内的某些字段
- 对一个实体类的某一个字段进行多线程操作,之前需要使用synchronized对这个对象加锁,粒度太大;使用上面的三个可以使锁的粒度变小,只对某一个字段进行更新
- 使用要求
- 更新的对象属性必须使用public volatile修饰符
- 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性
- 哪里使用到了volatile
- DCL
- AtomicIntegerFieldUpdater
5.原子操作增强类深度剖析
- DoubleAccumulator
- 一个或多个变量共同维护使用提供的函数更新的运行double值
- DoubleAdder
- 一个或多个变量共同维护最初的零和double总和
- LongAccumulator
- 一个或多个变量共同维护使用提供的函数更新的运行long值
- 提供了自定义的函数操作
LongAccumulator accumulator = new LongAccumulator( (x, y) -> x+y ,1); accumulator.accumulate(3); System.out.println(accumulator.get());
- LongAdder
- 一个或多个变量共同维护最初为零的总和为long
- 当多个线程更新用于收集统计信息,但不用于细粒度同步控制的目的的公共和时,此类通常优于AtomicLong,在低更新争用下,这两个类具有相似的特征,但在高争用的情况下,这一类的预期1吞吐量明显更高,但代价时空间消耗更高.
- 只能用来计算加法,且从零开始
案例
- 热点商品点赞计算器,点赞数加加统计,不要求实时精确
- 分别使用synchronized,AtomicLong,LongAdder,LongAccumulator.比较结果如下
longAdder的原理
- 使用
public static void main(String[] args) throws Exception { CountDownLatch count = new CountDownLatch(100); LongAdder longAdder = new LongAdder(); for (int i = 0; i < 100; i++) { new Thread( () -> { try { for (int i1 = 0; i1 < 1000; i1++) { longAdder.add(1); } }finally { count.countDown(); } }).start(); } count.await(); System.out.println(longAdder.sum()); }
- 继承结构
Striped64的重要成员属性以及方法
- base;类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
- collide: 表示扩容意向, false一定不会扩容, true可能会扩容
- cellsBusy: 自旋锁(通过CAS锁定) 在调整大小和/或创建cell时使用.初始化cells或者扩容cells需要获取锁,0:表示无锁状态1:表示其他线程已经持有了锁
- casCellsBusy(): 通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
- NCPU:当前计算机CPU数量,Cell数组扩容时会使用到
- getProbe():获取当前线程的hash值
- advanceProbe():重置当前线程的hash值
- transient volatile Cell[] cells:cell表,当非空时,大小是2的幂。
Cell
- 是java.util.concurrent.atomic.Striped64的一个静态内部类
源码分析
- longAdder并发量低的情况下等同于AtomicLong,会直接将数据直接累加到base上,
- 如果竞争压力急剧变化,LongAdder 的基本思路就是分散热点,空间换时间, 如果有竞争的话,内部维护了多个Cell变量,每个Cell里面有一个初始值为0的long型变量, 通过不同线程的id进行hash得到hash值,再根据hash值映射到这个数组cells的某个Cell (槽 )中,各个线程只对自己Cell(槽)中的那个值进行 CAS 操作。这样热点就被分散了,冲突的概率就小很多。
- 当多个线程竞争同一个cell激烈时,要对cell数组进行扩容
- 如果要获得完整的 LongAdder 存储的值,只要将各个槽中的变量值累加后再加上base的值就是最终的结果
- sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点.
- add()方法源码解析
public void add(long x) {
//as: 表示cells引用
//b: base值
//v: 表示当前线程命中的cell的期望值
//m: 表示cells数组长度
//a: 表示当前线程命中的cell
Striped64.Cell[] as; long b, v; int m; Striped64.Cell a;
/*
stop 1:true -> 说明存在竞争,并且cells数组已经初始化了,当前线程需要将数据写入到对应的cell中
false -> 表示cells未初始化,当前所有线程应该将数据写到base中第一个线程进来肯定 (as = cells) != null肯定为false,走的是casBase(b = base, b + x),以CAS方式的值更新base值,只有当CAS失败时才会走if
stop 2:对是判base这个值进行比较并交换,也就断有没有竞争,如果没有发生竞争,直接对base更新完成,如果没有交换成功,则存在竞争返回false,注意有一个取反的动作true -> 表示发生竞争了,可能需要重试或者扩容
false -> 表示当前线程cas替换数据成功
*/
if (
(as = cells) != null //stop 1
||
!casBase(b = base, b + x) //stop 2
) {
/*
进入的条件:
1.cells数组已经初始化了,当前线程需要将数据写入到对应的cell中
2.表示发生竞争了,可能需要重试或者扩容
*/
/*
是否有竞争:true -> 没有竞争
false -> 有竞争*/
boolean uncontended = true;
/*
stop 3:as == null || (m = as.length - 1)<0 代表 cells 没有初始化
stop 4:表示当前线程命中的cell为空,意思是还没有其他线程在同一个位置做过累加操作,应初始化一个cell
stop 5:表示当前线程命中的cell不为空, 然后在该Cell对象上进行CAS设置其值为v+x(x为该 Cell 需要累加的值),如果CAS操作失败,表示存在争用。也就是需要进行扩容了,走到if里面,并且uncontended为false,存在竞争,在longAccumulate里面会进行扩容
*/
if (as == null || (m = as.length - 1) < 0 || //stop 3
(a = as[getProbe() & m]) == null || //stop 4
!(uncontended = a.cas(v = a.value, v + x))) //stop 5
/*
进入的条件:一下三个条件满足一个即可进行if
1.cells 未初始化
2.当前线程对应下标的cell为空
3.当前线程对应的cell有竞争并且cas失败
*/
longAccumulate(x, null, uncontended);
}
}//
1.如果Cells表为空,尝试用CAS更新base字段,成功则退出;
2.如果Cells表为空,CAS更新base字段失败,出现竞争, uncontended为true, 调用
longAccumulate;
3.如果Cells表非空,但当前线程缺射的槽为空,uncontended为true, 调用longAccumulate;
4.如果Cells表非空,且前线程缺射的槽非空,CAS更新Cell的值,成功则返回,否则,
uncontended设为false,调用longAccumulate。//longAccumulate
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
//条件成立: 说明当前线程还未分配hash值
if ((h = getProbe()) == 0) {
//1.给当前线程分配hash值
ThreadLocalRandom.current(); // force initialization
//2.提取当前线程的hash值
h = getProbe();
//3.重新计算了当前线程的hash后认为此次不是一次竞争,都未初始化,肯定还不存在竞争激烈wasUncontended状态true
wasUncontended = true;
}
//扩容意向,collide=true 可以扩容,collide=false 不可扩容
boolean collide = false; // True if last slot nonempty
//自旋,一直到操作成功
for (;;) {
//as: 表示cells引用
//a: 当前线程命中的cell
//n: cells数组长度
//a: 表示当前线程命中的cell的期望值
Striped64.Cell[] as; Striped64.Cell a; int n; long v;
//CASE1: cells数组已经初始化了,当前线程将数据写入到对应的cell中
if ((as = cells) != null && (n = as.length) > 0) {
//CASE1.1: true 表示下标位置的 cell 为 null,需要创建 new Cell
if ((a = as[(n - 1) & h]) == null) {
// cells 数组没有处于创建、扩容阶段
if (cellsBusy == 0) { // Try to attach new Cell
Striped64.Cell r = new Striped64.Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Striped64.Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)//创建、扩容成功,退出自旋
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
// CASE1.2:当前线程竞争修改cell失败,这里只是重新设置了这个值为true,紧接着执 行abvanceProbe(h)重置当前线程的hash,重新循环
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// CASE 1.3:当前线程 rehash 过 hash 值,CAS 更新 Cell,如果CAS成功则直接跳出循环
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// CASE1.4:判断是否可以扩容
// CASE1.4.1:n >= NCPU n是cells数组的长度
// true -> cells数组长度已经 >= cpu核数,不可进行扩容,把扩容意向改为false
// false -> 可扩容
// CASE1.4.2:cells != as
// true -> 其它线程已经扩容过了,当前线程rehash之后重试即可
// false -> 未有线程对cells进行修改
// 如果n大于CPU最大数量,并且已经被修改过,则不可扩容,通过adbanceProbe(h)修改线程的probe再重新尝试else if (n >= NCPU || cells != as)
collide = false; // 把扩容意向改为false,即不可扩容
// CASE 1.5:设置扩容意向为 true,但是不一定真的发生扩容
else if (!collide)
collide = true;
//CASE1.6:真正扩容的逻辑
// CASE1.6.1:cellsBusy == 0
// true -> 表示cells没有被其它线程占用,当前线程可以去竞争锁
// false -> 表示有其它线程正在操作cells
// CASE1.6.2:casCellsBusy()
// true -> 表示当前线程获取锁成功,可以进行扩容操作
// false -> 表示当前线程获取锁失败,当前时刻有其它线程在做扩容相关的操作
else if (cellsBusy == 0 && casCellsBusy()) {
try {
//重复判断一下当前线程的临时cells数组是否与原cells数组一致(防止有其它线程提前修改了cells数组,因为cells是volatile的全局变量)
if (cells == as) { // Expand table unless stale
//n << 1 表示数组长度翻一倍
Striped64.Cell[] rs = new Striped64.Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
//扩容后,将扩容意向置为false
collide = false;
continue; // Retry with expanded table
}
//重置当前线程hash值
h = advanceProbe(h);
}
//CASE2:cells 还未初始化(as 为 null),并且 cellsBusy 加锁成功
// CASE2.1:判断锁是否被占用
// 0-> 表示当前未被占用,处于无锁状态
// 1 -> 表示当前已被占用,其他线程持有了锁
// CASE2.2:因为其它线程可能会在当前线程给as赋值之后修改了cells
// true -> cells没有被其它线程修改
// false -> cells已经被其它线程修改
// CASE2.3:获取锁
// true -> 获取锁成功 会把cellsBusy = 1
// false -> 表示其它线程正在持有这把锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try {
//双重检查,防止其它线程已经初始化,当前线程再次初始化,会导致数据丢失
// Initialize table
if (cells == as) {
Striped64.Cell[] rs = new Striped64.Cell[2];
rs[h & 1] = new Striped64.Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
//CASE3:多个线程尝试CAS修改失败的线程会走到这个分支;如果当前线程 cellsBusy 加锁失败,表示其他线程正在初始化 cells
//所以当前线程将值累加到 base,注意 add(…)方法调用此方法时 fn 为 null
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}//sum():
sum在执行的时候,并没有限制对base和Cells的更新,所以LongAdder不是强一致性的,它 是最终一致性的.
首先,最终返回的sum局部变量,初始被复制为base,而最终返回的时候,很可能base已经被 更新,而此时局部变量sum不会被更新,造成不一致.其次这里对cell的读取也无法
保证是最后一次写入的值.所以,sum方法在没有并发的情况下,可以获得正确的结果
public long sum() { Cell[] cs = cells; long sum = base; if (cs != null) { for (Cell c : cs) if (c != null) sum += c.value; } return sum; }
图形化总结
总结
- AtomicLong
- 线程安全,可允许一些性能损耗,要求高精度时可使用
- 保证精度,性能代价
- AtomicLong时多个线程针对某个热点值value进行原子操作
- 原理:CAS+字段;主要方法:incrementAndGet
- 使用场景:低并发下的全局计算,能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题.
- 缺陷:高并发后性能急剧下降,Atomic的自选会成为瓶颈
- N个线程CAS操作修改线程的值,每次只有一个成功,其它N- 1失败,失败
的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了。
- N个线程CAS操作修改线程的值,每次只有一个成功,其它N- 1失败,失败
- LongAdder
- 场景:当需要在高并发下有较好的性能表现,且对值得精确度要求不高时,可以使用
- 保证性能,精度代价(Sum求和后还有计算线程修改结果的化,最后结果不够准确)
- LongAdder是每个线程拥有自己的槽,各个线程一般支队自己槽中的那个值进行CAS操作
- 原理:CAS+Base+Cell数组分散;空间换时间分散了热点数据
Unsafe类
- Unsafe 是位于 sun.misc 包下的一个类,Unsafe 提供了CAS 方法,直接通过native 方式(封装 C++代码)调用了底层的 CPU 指令 cmpxchg。
- Unsafe类,翻译为中文:危险的,Unsafe全限定名是 sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类
- Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类直接存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法.
-
注意Unsafe类中的所有的方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
Unsafe 提供的 CAS 方法
主要如下:定义在 Unsafe 类中的三个 “比较并交换”原子方法
/*
@param o 包含要修改的字段的对象
@param offset 字段在对象内的偏移量
@param expected 期望值(旧的值)
@param update 更新值(新的值)
@return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);
- Unsafe 提供的 CAS 方法包含四个入参: 包含要修改的字段对象、字段内存位置、预期原值及新值。在执行 Unsafe 的 CAS 方法的时候,这些方法首先将内存位置的值与预期值(旧的值)比较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值,并返回 true ;如果不相匹配,处理器不做任何操作,并返回 false 。
获取属性偏移量
Unsafe 提供的获取字段(属性)偏移量的相关操作,主要如下:
/**
* @param o 需要操作属性的反射
* @return 属性的偏移量
*/
public native long staticFieldOffset(Field field);
public native long objectFieldOffset(Field field);
- staticFieldOffset 方法用于获取静态属性 Field 在 Class 对象中的偏移量,在 CAS 操作静态属性时,会用到这个偏移量。
- objectFieldOffset 方法用于获取非静态 Field (非静态属性)在 Object 实例中的偏移量,在 CAS 操作对象的非静态属性时,会用到这个偏移量。
根据属性的偏移量获取属性的最新值
/**
* @param o 字段所属于的对象实例
* @param fieldOffset 字段的偏移量
* @return 字段的最新值
*/
public native int getIntVolatile(Object o, long fieldOffset);
CAS
CAS是什么
- CAS的全称是Compare-And-Swap,它是一条CPU并发原语.它的功能是判断内存某个位置的值是否为预期值,如果是则修改为新的值,这个过程是原子性的.
- CAS并发原语体现在JAVA语言中的就是Unsafe类中的各个方法,调用Unsafe类中的CAS方法,JVM就会帮我们实现出CAS汇编指令.这是一种完全依赖于硬件的功能,最底层还是通过硬件保证了原子性和可见性;由于CAS是-一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说,CAS是一条CPU的原子指令不会造成数据不-致问题.
- Unsafe提供的CAS方法底层实现即为CPU指令cmpxchg,执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起synchronized重量级锁,这里的排他时间要短的多,所以在多线程情况下性能会比较好
如果当前值==预期值,则自动将该值设置为给定的更新值。
参数:
expect–期望值
更新–新值
返回:如果成功,则为true。假返回表示实际值不等于预期值。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}如果期望值(线程操作的是主内存变量的副本, expect=原内存中值, update=当前线程中的方法对副本的修改)等于主内存中值,就修改为update保证在自己之前没有别的线程修改过变量的值
- CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)
- CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
- 通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。
- 类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法可以对该操作重新计算
注意:要使用cas修改某个对象属性:必须知道属性在对象内存空间的哪个位置(偏移量)
CAS底层原理
底层原理:自旋锁, Unsafe类
public class AtomicInteger extends Number implements java.io.Serializable { //得到魔法类Unsafe private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe(); //得到value在AtomicInteger中的位置即在内存中的位置 private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value"); //数量值 Value用volatile修饰保证了对象之间的内存可见性 private volatile int value; //初始化value public AtomicInteger(int initialValue) { value = initialValue; } //初始化值为0 public AtomicInteger() { }//获得当前对象的地址值为VALUE位置的值,并进行加1操作
public final int getAndIncrement() { //this:当前对象 //VALUE:value字段的内存地址,Unsafe就是根据内存偏移地址获取数据的 //1:步长 return U.getAndAddInt(this, VALUE, 1); }
o:当前对象
offset:地址偏移量
delta:常量1
v; 局部变量(存放从内存中取出来的值)
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { //getIntVolatile见名知意保证了可见性 v = getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; }!weakCompareAndSetInt(o, offset, v, v + delta):
如果当前对象(var1)这个地址(var2)的值跟var5相同,就把这个地址的(var2)值修改为var5+var4并且返回true,再取反,返回false,退出while循环;
假设这个东西已经被别人改过了, 当前对象(var1)这个地址(var2)的值跟var5的值不相同 返回false 取反 整体为true 陷入do while循环(目前的快照值和真实的物理内存中的值不一样)
失败后获取到的是其他线程修改后的最新的值
自旋锁
CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,知道获取,这样做的好处是减少上下文切换的消耗,缺点是循环会消耗CPU.
实现自旋锁
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock(){
//获得锁的线程
Thread thread = Thread.currentThread();
System.out.println("线程\t"+thread.getName()+"试图获取锁");
//如果内存中的值为空(),跳出while循环;如果不为空,一直while循环
while (!atomicReference.compareAndSet(null,thread)){}
System.out.println("线程\t"+thread.getName()+"获取到锁");
}public void unMyLock(){
Thread thread = Thread.currentThread();
//解锁操作
System.out.println("线程\t"+thread.getName()+"释放锁");
atomicReference.compareAndSet(thread,null);}
public static void main(String[] args) throws InterruptedException {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread( () ->{
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.unMyLock();
},"AAA").start();TimeUnit.SECONDS.sleep(1);
new Thread( () ->{
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.unMyLock();
},"BBB").start();
}
}//解释
线程AAA进行首先获得进来,当前atomicReference为null,!atomicReference.compareAndSet(null,thread)返回false退出while循环,此时线程BBB进来,因为线程AAA已经更改了atomicReference的值,会一直卡在while循环,当线程AAA执行完atomicReference.compareAndSet(thread,null);之后线程BBB会退出while循环
整体流程
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
- Atomiclnteger见面的value原始值为3.即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
- 线程A通过getintVolatil(var1, var2)拿到value值3, 这时线程A被挂起。
- 线程B也通过etltVlatie(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4.线程B打完收工,oK。
- 这时线程A恢复,执行compareAndSwaplnt方法比较。 发现自己手里的值数字3和主内存的值数字4不一-致, 说明该值已.经被其它线程抢先- - 步修改过了,那A线程本次修改失败。只能重新读取重新来-遍 了。
- 线程A重新获取value值, 因为变量value被volatile修饰, 所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。
CAS和Synchronized对比
- Synchronized关键字会让没有得到锁资源的线程进入Blocked状态,而后在争夺到锁资源后恢复为Runing状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
- 另外,从思想上来说,Synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守。而CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
- 尽管JAVA 1.6之后为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然比较低(Synchronized转变为重量级锁之前,也会采用CAS机制)。所以面对这种情况,我们就可以使用Java中的原子操作类。
- 所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。而Atomic操作类的底层正是用到了“CAS机制”。
总结
-
CAS思想靠的是unsafe类的cpu底层的原语来保证原子性
- Sync需要加锁,保证了一致性,同一时间段只允许一个线程来访问,但是并发性下降了,CAS用的是do while循环,可以反复的进行比较直到成功的那一刻为止,既保证了一致性,又提高了并发性
CAS的缺点
CPU开销过大
- 循环时间长,开销大:我们可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//getIntVolatile见名知意保证了可见性v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; }
- 解决CAS 恶性空自旋的较为常见的方案为
- 分散操作热点,使用 LongAdder 替代基础原子类 AtomicLong。
- 使用队列削峰,将发生 CAS 争用的线程加入一个队列中排队,降低 CAS 争用的激烈程度。JUC 中非常重要的基础类 AQS(抽象队列同步器)就是这么做的。
- 关于这部分的详细介绍可以点击这里查看
不能保证代码块的原子性
- CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证多个共享变量共同进行原子性的更新,就不得不使用Synchronized了。
ABA问题及解决方案
- CAS算法实现的一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差会导致数据的变换;比如说一个线程one从内存V中取出A,这时候另一个线程two也从内存中取出A,并将A变成了B,然后又将数据变成了A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功.
- 尽管线程one得CAS操作成功,但是不代表这个过程就是没有问题的(CAS只管开局和结尾,不注重中间的细节) 线程A拿到的1不再是以前的1,线程B对其进行修改过
- 解决办法: AtomicStampedReference
- 可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
public class CASDemo1 {
public static void main(String[] args) {
AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1, 1);
new Thread( ()->{
int stamp = reference.getStamp(); //获得版本号
System.out.println("a1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println(Thread.currentThread().getName()+reference.compareAndSet(1, 2,
reference.getStamp(), reference.getStamp() + 1)); System.out.println("a2=>"+reference.getStamp()); System.out.println(Thread.currentThread().getName()+reference.compareAndSet(2, 1,
reference.getStamp(), reference.getStamp() + 1));
System.out.println("a3=>"+reference.getStamp());
},"a").start();
new Thread( ()->{//因为stamp已经获取到,但是前一个线程对stamp已经过了修改,会导致下面更新不成功
int stamp = reference.getStamp(); //获得版本号
System.out.println("b1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println(Thread.currentThread().getName()+reference.compareAndSet(1, 6,
stamp, stamp + 1)); System.out.println("b1=>"+reference.getStamp());
},"b").start();