为啥先说JMM,由于CAS的实现类中维护的变量都被volatile修饰, 这个volatile 是遵循JMM规范(不是百分百遵循,下文会说)实现的保证多线程并发访问某个变量实现线程安全的手段java
一连串的知识点慢慢缕c++
首先说什么是JMM, JMM就是你们所说的java的内存模型, 它是人们在逻辑上作出的划分, 或者能够将JMM当成是一种规范, 有哪些规范呢? 以下安全
JVM运行的实体是线程, 每个线程在建立以后JVM都会为其建立一个工做空间, 这个工做空间是每个线程之间的私有空间, 而且任何两条线程之间的都不能直接访问到对方的工做空间, 线程之间的通讯,必须经过共享空间来中转完成多线程
JMM规定全部的变量所有存在主内存中,主内存是一块共享空间,那么若是某个线程相对主内存中共享变量作出修改怎么办呢? 像下面这样:并发
JMM还规定以下:函数
问题引入性能
这时候若是多个线程并发按照上面的三步走去访问主内存中的共享变量的话就会出现线程安全性的问题, 好比说 如今主内存中的共享变量是c=1, 有AB两个线程去并发访问这个c变量, 都想进行c++, 如今A将c拷贝到本身的工做空间进行c++, 因而c=2 , 于此同时线程B也进行c++, c在B的工做空间中=2, AB线程将结果写回工做空间最终的结果就是2, 而不是咱们预期的3优化
相信怎么解决你们都知道, 就是使用JUC,中的原子类就能规避这个问题this
而原子类的底层实现使用的就是CAS技术atom
CAS(compare and swap) 顾名思义: 比较和交换,在JUC中原子类的底层使用的都是CAS无锁实现线程安全,是一门很炫的技术
以下面两行代码, 先比较再交换, 即: 若是从主内存中读取到的值为4就将它更新为2019
AtomicInteger atomicInteger = new AtomicInteger(4); atomicInteger.compareAndSet(4,2019);
跟进AtomicInteger
的源码以下, 底层维护着一个int 类型的 变量, (固然是由于我选择的原来类是AtomicInteger类型), 而且这个int类型的值被 volatile 修饰
private volatile int value; /** * Creates a new AtomicInteger with the given initial value. * * @param initialValue the initial value */ public AtomicInteger(int initialValue) { value = initialValue; }
volatile是JVM提供的轻量的同步机制, 为何是轻量界别呢? , 刚才在上面说了JMM规范中提到了三条特性, 而JVM提供的volatile仅仅知足上面的规范中的 2/3, 以下:
单独的volatile是不能知足原子性的,即以下代码在多线程并发访问的状况下依然会出现线程安全性问题
private volatile int value; public void add(){ value++; }
那么JUC的原子类是如何实现的 能够知足原子性呢? 因而就不得不说本片博文的主角, CAS
咱们跟进AtomicInteger
中的先递增再获取的方法 incrementAndGet()
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
经过代码咱们看到调用了Unsafe类来实现
什么是Unsafe类?
进入Unsafe类,能够看到他里面存在大量的 native方法,这些native方法所有是空方法,
这个unsafe类其实至关于一个后门,他是java去访问调用系统上 C C++ 函数类库的方法 以下图
继续跟进这个方法incrementAndGet()
因而咱们就来到了咱们的主角方法, 关于这个方法却是不难理解,主要是搞清楚方法中的var12345到底表明什么就行, 以下代码+注释
var1: 上一个方法传递进来的: this,即当前对象 var2: 上一个方法传递进来的valueOffset, 就是内存地址偏移量 经过这个内存地址偏移量我能精确的找到要操做的变量在内存中的地址 var4: 上一个方法传递进来的1, 就是每次增加的值 var5: 经过this和内存地址偏移量读取出来的当前内存中的目标值 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; }
注意它用的是while循环, 相对if(flag){} 这种写法会多一次判断, 总体的思路就是 在进行修改以前先进行一次比较,若是读取到的当前值和预期值是相同的,就自增,不然的话就继续轮询修改
经过上面的过程, 其实就能总结出CAS的底层实现原理
补充: CAS经过Native方法的底层实现,本质上是操做系统层面上的CPU的并发原语,JVM会直接实现出汇编层面的指令,依赖于硬件去实现, 此外, 对于CPU的原语来讲, 有两条特性1,一定连续, 2.不被中断
优势:
它的底层咱们看到了经过do-while 实现的自旋锁来实现, 就省去了在多个线程之间进行切换所带来的额外的上下文切换的开销
缺点:
什么是ABA问题
咱们这样玩, 仍是AB两个线程, 给AtomicInteger
赋初始值0
A线程中的代码以下:
Thread.sleep(3000); atomicInteger.compareAndSet(0,2019);
B线程中的代码以下:
atomicInteger.compareAndSet(0,1); atomicInteger.compareAndSet(1,0);
AB线程同时启动, 虽然最终的结果A线程能成果的将值修改为2019,,可是它不能感知到在他睡眠过程当中B线程对数据进行过改变, 换句话说就是A线程被B线程欺骗了
ABA问题的解决--- AtomicStampedRefernce.java
带时间戳的原子引用, 实现的机制就是经过 原子引用+版本号来完成, 每次对指定值的修改相应的版本号会加1, 实例以下
// 0表示初始化, 1表示初始版本号 AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(0, 1); reference.getStamp(); // 获取版本号 reference.attemptStamp(1,2); // 期待是1, 若是是1就更新为2
JUC中咱们能够找到像AtomicInteger
这样已经定义好了实现类, 可是JUC没有给咱们提供相似这样 AtomicUser
或者 AtomicProduct
这样自定义类型的原子引用类型啊, 不过java仍然是提供了后门就是 原子引用类型
使用实例:
User user = getUserById(1); AtomicReference<User> userAtomicReference = new AtomicReference<User>(); user.setUsername("张三"); userAtomicReference.compareAndSet(user,user);
欢迎关注我, 会继续更新笔记