文章目录
- 1、CAS的简介
- 1.1、什么是CAS
- 1.2、使用CAS的前后对比
- 1.3、CAS如何做到不加锁的情况,保证数据的一致性
- 1.4、什么是Unsafe类
- 1.5、CAS方法参数详解
- 1.6、CAS的原理
- 1.7、 CAS的缺点
- 2、原子操作类
- 2.1、基本类型原子类
- 2.2、数据类型原子类
- 2.3、引用类型原子类
- 2.4、对象的属性修改原子类
- 2.4.1、它能帮我们解决什么问题
- 2.4.2、使用要求
- 2.5、原子操作增强类(jdk1.8才有)
- 3、LongAdder效率这么快(源码分析篇)
- 3.1、几个比较重要的成员变量以及方法
- 3.2、LongAdder为什么这么快
- 3.3、源码解析
1、CAS的简介
1.1、什么是CAS
CAS(Compare And Swap)的缩写,中文翻译成比较并交换,实现并发算法时常用到一种技术;他包含了3个操作数 ----- 内存位置,
,预期原值,更新值。执行CAS操作的时候,将内存位置的值与原值进行比较
- 如果相匹配,那么处理器会自动将该位置的值更新为新值
- 如果不匹配,处理不做任何操作,多个线程同时执行CAS操作,只有1个会成功
1.2、使用CAS的前后对比
没有CAS的时候, 我们利用sync和voliate保证符合操作的原子性
用了原子操作类后之后的操作,保证了i++的原子性,没有加入sync重量级别的锁
1.3、CAS如何做到不加锁的情况,保证数据的一致性
- CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
- 它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
- CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,
Unsafe
提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。 - 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,
- 所以在多线程情况下性能会比较好。
1.4、什么是Unsafe类
- Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地方法来访问,Unsafe相当于是一个后门,基于该类可以直接操作特定内存你的数据.Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接去操作内存,因为java中Cas操作的执行依赖于Unsafe方法
- CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用诒范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说
CAS是一条CPU的原子指令
,不会造成所谓的数据不一致问题。
总结:CAS其实是Unsafe提供的一个方法,并且CAS是系统原语,本身就有执行过程不被中断的特性,天生就有保护原子性的特性
1.5、CAS方法参数详解
/**
var1: 表示要操作的对象
var2:要操作对象属性地址偏移量
var3:表示需要修改数据的期望值
var4:需要修改为的新值
**/
boolean compareAndSwapObject(Object var1,long var2,Object var3,Object var4)
1.6、CAS的原理
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
- 1 AtomicInteger里面的value原始值为,即主内存中AtomiclInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
- 2线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起
- 3线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
- 4这时线租A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
- 5线程A重新获取valud值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
1.7、 CAS的缺点
-
问题1 : 因为do While 循环,所以可能循环时间长,开销比较大
解释:因为cas是处于do while循环中的,如果一直没有修改成果,则CPU处于空转状态;开销比较大; -
问题2 :引出ABA问题
问题的产生
- AB两个线程做操作,主内存的值为1,此时他们进行拷贝,他们各自的空间的值都为1
- A线程把主内存的值1改为2,然后又该1,
- 此时B过来来修改至根据cas的期望值,他发现1就是他所期望的值,他认为并没有人对主内存进行修改过
上面过程A线程把数据从1->2->1 ,到了B线程读取的时候,进行比较比较他觉得这数据是没有人动过的,这是不符合CAS的原理的.他只管开头和结尾,不关心中心的内容,这是不对的
如何解决ABA问题
AtomicStampedReference[关心改了多少次,参考下一小节,原子类]
2、原子操作类
原子操作类,有很多这里主要把他们分类成如下几类 ,下面的代码示例中,会从中抽出来最经典的进行讲解
2.1、基本类型原子类
- AtomicInteger (讲解案例)
public class TestMain {
static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
},"t1").start();
}
// Thread.sleep(2000);
System.out.println("获取到的值是:"+atomicInteger.get());
}
public static void add(){
atomicInteger.getAndIncrement();
}
}
虽然使用了atomicInteger,但是输出结果并不是10000;
原因: 还没有等t1 线程计算完成的时候,就已经主线程就已经获取结果了, 此时我们可以使用CountDownLatch ,让主线程等待子线程结束完毕,在运行
public class TestMain {
static AtomicInteger atomicInteger = new AtomicInteger(0);
static CountDownLatch countDownLatch = new CountDownLatch(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
countDownLatch.countDown();
}, "t1").start();
}
// Thread.sleep(2000);
countDownLatch.await();
System.out.println("获取到的值是:" + atomicInteger.get());
}
public static void add() {
atomicInteger.getAndIncrement();
}
}
- AtomicBoolean
- AtomicBoolean
2.2、数据类型原子类
- AtomicIntegerArray(讲解案例)
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
//对下标为0的位置 ,增加100
atomicIntegerArray.addAndGet(0,100);
//对下标为1的元素 ,加1
atomicIntegerArray.getAndIncrement(1);
System.out.println(atomicIntegerArray.get(0));
System.out.println(atomicIntegerArray.get(1));
输出结果
100
1
- AtomicLongArray
- AtomicReferenceArray
2.3、引用类型原子类
- AtomicReference
- AtomicStampedReference[修改过几次,利用版本号的机制],参考ABA问题
- AtomicMarkableReference[有没有修改过]
public class TestMain {
static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<>(100, false);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
boolean marked = atomicMarkableReference.isMarked();
System.out.println(Thread.currentThread().getName()+"获取的标志位为"+marked);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean compareAndSet = atomicMarkableReference.compareAndSet(100, 1000, marked, !marked);
System.out.println(Thread.currentThread().getName()+"修改为1000,是否成功"+compareAndSet);
},"t1").start();
new Thread(()->{
boolean marked = atomicMarkableReference.isMarked();
System.out.println(Thread.currentThread().getName()+"获取的标志位为"+marked);
boolean compareAndSet = atomicMarkableReference.compareAndSet(100, 2000, marked, !marked);
System.out.println(Thread.currentThread().getName()+"修改为2000,是否成功"+compareAndSet);
},"t2").start();
Thread.sleep(2000);
System.out.println("主线程获取的值为"+atomicMarkableReference.getReference());
}
}
输出结果
t1获取的标志位为false
t2获取的标志位为false
t2修改为2000,是否成功true
t1修改为1000,是否成功false
主线程获取的值为2000
2.4、对象的属性修改原子类
2.4.1、它能帮我们解决什么问题
作用: 以一种线程安全的方式操作非线程安全对象内的某些字段
没有使用前遇到的问题
class Book {
private Integer id;
private String name;
public synchronized void add(){
id++;
}
}
在之前我们只是想修改1个id值,却直接加了synchronized; synchronized锁的是一个对象? 有没有什么办法只锁Id呢?
2.4.2、使用要求
-
更新的对象必须使用public volatile修饰
-
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法NewUpdater()创建一个更新器,并且需要设置想要更新的类和属性
-
AtomicIntegerFieldUpdater
-
AtomicLongFieldUpdater
-
AtomicReferenceFieldUpdater
2.5、原子操作增强类(jdk1.8才有)
- DoubleAccumulator
- DoubleAdder
- LongAccumulator(提供了自定义的函数操作)
//这个0就是x,这个1就是y
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
//0+1
longAccumulator.accumulate(1);
//1+2
longAccumulator.accumulate(2);
System.out.println(longAccumulator.get());
输出结果:3
- LongAdder(只能用来计算加法,且之能从零开始计算)
LongAdder longAdder = new LongAdder();
longAdder.increment();
longAdder.increment();
longAdder.increment();
longAdder.increment();
System.out.println(longAdder.sum());
输出结果 4
LongAdder和和AtomicInterget的性能对比效率对比
package com.tvu.interruput;
//需求:50个线程,每个线程100W次,总点赞数出来;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
class Reader {
Long number = 0L;
/**************sync的效率************************/
public synchronized void syncAddClick() {
number++;
}
public Long getSyncNumber() {
return number;
}
/**************Atomic的效率************************/
AtomicLong atomicLong = new AtomicLong();
public void atomicAddClick() {
atomicLong.getAndAdd(1);
}
public Long atomicGetClick() {
return atomicLong.get();
}
/**************LongAdder的效率************************/
LongAdder longAdder = new LongAdder();
public void adderClick() {
longAdder.increment();
}
public Long getAddNumber() {
return longAdder.sum();
}
/**************LongAccumulator的效率************************/
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
public void accMulatorClick() {
longAccumulator.accumulate(1);
}
public long getAccNumber() {
return longAccumulator.get();
}
}
public class TestMain {
public static final int _1W = 10000;
public static final int threadNumber = 50;
Reader reader = new Reader();
static CountDownLatch syncCountDownLatch = new CountDownLatch(threadNumber);
public static void main(String[] args) throws InterruptedException {
Reader reader = new Reader();
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(() -> {
for (int j = 0; j < _1W * 1000; j++) {
reader.accMulatorClick();
}
syncCountDownLatch.countDown();
}, "t" + i).start();
}
syncCountDownLatch.await();
long endTime = System.currentTimeMillis();
//sync耗时时间为12395 输出结果为500000000
//System.out.println("sync耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.getSyncNumber());
//atomic耗时时间为7988 输出结果为500000000
//System.out.println("atomic耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.atomicGetClick());
//AddLong耗时时间为599 输出结果为500000000
//System.out.println("AddLong耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.getAddNumber());
// AccMulator耗时时间为642 输出结果为500000000
System.out.println("AccMulator耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.getAccNumber());
}
}
此时我们不禁好奇,为什么LongAdder效率这么快呢?(详情参考LongAdder源码解析)
3、LongAdder效率这么快(源码分析篇)
3.1、几个比较重要的成员变量以及方法
Striped64(他是LongAdder的父类),他主要包含了如下几个比较重要的属性和方法
- Cell[] cells 数组,为2的幂,方便以后位运算
- base:类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
- cellsBusy:初始化cells或者扩容cells需要获取锁,0:表示无锁状态 1:表示其他线程已经持有了锁
- collide:表示扩容意向,false一定不会扩容,true可能会扩容。
- casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
- NCPU:当前计算机CPU数量,Cell数组扩容时会使用到
- getProbe():获取当前线程的hash值
- advanceProbe():重置当前线程的hash值
3.2、LongAdder为什么这么快
- AtomicInteger 慢的原因: 当线程比较多的时候,利用cas,此时空转的线程就会增多,系统cpu就会有负担
- 利用Cell[]数组分散热点,将value分散到不同的Cell数组中,不同线程会命中到不同的槽位中,各个线程只对自己的槽中那个值进行cas操作,这样热点就分散了,冲突的概率就减少许多了;如果想要获取真正的Long值,只要将各个槽的变量值累加返回即可
3.3、源码解析
第一次进longAccumulate: 数组的初始化
- 如果线程竞争不激烈,则直接在base基础上进行cas操作
- 如果线程竞争不激烈,则直接在base基础上进行cas操作
- 初始化阶段,创建2个cell数组,进行赋值
第二次进来,数组的赋值
- 确定槽位,进行cas赋值
2.这里是直接对目前的2个槽位进行赋值,没有进longAccumulate
第三次进来,进longAccumulate,需要根据Cell的状态进入不用的if代码块
进来的前提 : 目前的有了2个cell槽位,依旧竞争激烈
此时会根据不同的cell状态,进入到不同的代码分支逻辑模块; 我们先看Cell[]数组已经初始化的情况
状态1: 如果Cell[]数组已经初始化
- 分支1 有槽位,但是还没有值,进行赋值
- 分支2 槽位cas修改失败,重新抢占
分支3: 有槽位,且有值,直接进行修改
分支4:如果槽位大于cpu的数量,则不扩容
分支5:新建一个cell数组,进行扩容,迁移
状态2:Cell[]数组未初始化[首次新建]
状态3:Cell[]数组正在初始化
如果多个线程进行casCellsBusy 修改锁状态失败,则会进入到这个分支;cell初始化不了,就把值加给base进行累加