Java看AtomicInteger源码学习CAS算法

一、线程

1.1 线程的概述

  • 八个运行程序就是三个进度,而线程是经过中单独运维的子职务
  • 线程是操作系统执行流中的细微单位,三个经过能够有几个线程,那个线程与该进程共享同3个内部存款和储蓄器空间
  • 线程是系统独立调度和分担CPU的中坚单位,平时有妥帖、运营、阻塞三种基本情状
  • 随着硬件水平的增高,八线程能使系统的周转功用获得巨大的增进,同时异步操作也增多复杂度和各类并发难题

1.2 三十二线程的危机之一上下文切换

上下文切换:
CPU通过时间片分配算法来循环执行义务,当前任务执行三个岁月片后会切换成下一个职务。不过,在切换前会保存上三个职责的图景,以便下次切换回这一个义务时得以再一次加载那么些职务的意况。全体任务从保存到再加载的长河就是贰回上下文切换
八线程质量难题:出于线程有开创和上下文切换的开发,在十六线程环境下,那种支付对时间和能源的施用都以二个相当的大的承负,很恐怕造成出现职务履行进程还不如串行快
削减上下文切换: 无锁并发编制程序、CAS算法、减弱并发、使用最少线程、协程
.

2、并发编制程序中的锁

2.1 悲观锁

Java在JDK1.5事先都以靠synchronized关键字确认保证同步的,那种通过动用同样的锁定协议来协调对共享状态的走访,能够有限扶助无论哪个线程持有共享变量的锁,都应用独占的艺术来拜访那些变量。独占锁其实就是一种悲观锁,所以能够说synchronized是自己瞎着急锁。存在以下难点:
在十六线程竞争下,加锁、释放锁会促成相比多的上下文切换和调度延时,引起品质难点;3个线程持有锁会导致其余具有须要此锁的线程挂起;
假如七个先期级高的线程等待二个先行级低的线程释放锁会促成优先级倒置,引起质量风险。

2.2 乐观锁

乐观锁( Optimistic
Locking)其实是一种沉思。相对悲观锁而言,乐观锁即使认为数额貌似景观下不会造成争辩,所以在数码进行提交更新的时候,才会规范对数据的争执与否举办检查和测试,要是发现争论了,则让重临用户错误的音信,让用户决定哪些去做。
地点提到的乐观锁的定义中实际上早就演讲了她的切实可行达成细节:首要便是五个步骤:争持检测和数据更新。其落到实处情势有一种相比典型的便是Compare
and Swap(CAS)。

三 、无锁执行者CAS

3.1 无锁的概念

在商量无锁概念时,总会涉嫌起乐观派与悲观派,对于乐观派而言,他们觉得工作总会往好的样子前行,总是认为坏的情景发生的可能率越发小,可以无所顾忌地劳作,但对于悲观派而已,他们总会觉得升高情况假设不及时间控制制,现在就不可能挽回了,就算不可能挽回的局面大概不容许发生。那二种流派映射到出现编制程序中就好似加锁与无锁的国策,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们连接觉得每一回访问共享能源时总会产生争辩,因而必须对每2遍数据操作实施加锁策略。而无锁则总是尽管对共享财富的拜访尚未冲突,线程能够不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则应用一种叫做CAS的技艺来担保线程执行的安全性,这项CAS技术就是无锁策略实现的重大,下边我们更为明白CAS技术的稀奇古怪之处。

3.2 CAS

CAS是项乐观锁技术,当多少个线程尝试利用CAS同时创新同一个变量时,唯有中间3个线程能更新变量的值,而别的线程都未果,退步的线程并不会被挂起,而是被告知此次竞争中失利,并得以另行尝试。其算法核心绪想如下

履行函数:CAS(V,E,N)

其包含3个参数

  • V表示要更新的变量

  • E表示预期值

  • N表示新值

假定V值等于E值,则将V的值设为N。若V值和E值分化,则印证已经有任何线程做了履新,则当前线程什么都不做。通俗的驾驭正是CAS操作供给我们提供贰个期望值,当期望值与近年来线程的变量值相同时,表明还没线程修改该值,当前线程能够拓展改动,也正是履行CAS操作,但即使期望值与最近线程不符,则证实该值已被其它线程修改,此时不执行更新操作,但能够选用重复读取该变量再尝试重新修改该变量,也能够放任操作,原理图如下:

CAS原理图

四、Java对CAS的支持

我们以java.util.concurrent中的AtomicInteger为例,看一下在不应用锁的境况下是哪些确认保障线程安全的。

4.1 AtomicInteger 类的变量以及静态代码块

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    //获取unsafe对象
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    //value在内存中的地址偏移量  
    private static final long valueOffset;

    static {
        try {
            //获得value的内存地址偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //当前对象代表的值,注意是volatile(**下面会解释该关键字**)
    private volatile int value;

4.2 深究Unsafe类

从这么些类的名字Unsafe上的话那几个类正是三个不安全的类,它存在于sun.misc包中,其内部方法操作能够像C的指针一样一直操作内部存款和储蓄器,单从名称看来就能够知晓该类是非安全的,毕竟Unsafe拥有着就像是于C的指针操作,因而接连不该首先应用Unsafe类,Java官方也不建议直接选取的Unsafe类,也不开放给用户一直运用的(当然我们如故得以通过其余部分艺术用到)。Java
9上将移除
Sun.misc.Unsafe,
原版的书文链接:https://yq.aliyun.com/articles/87265

@CallerSensitive
    public static Unsafe getUnsafe() {

        //得到调用者的class对象,这里即是Unsafe
        Class arg = Reflection.getCallerClass();

       //判断调用Unsafe的类是否是BootstrapClassLoader加载的类 
        if (!VM.isSystemDomainLoader(arg.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

那几个类本人是单例的,需求经过静态方法获取唯一实例。遵照代码知道应该是透过类加载器限制。一般大家写的类都以由Application
ClassLoader(sun.misc.Launcher$AppClassLoader)举办加载的,层级相比较低,那里的SystemDomainLoader便是BootstarpClassLoader(C++写的),也便是加载rt.jar里面的类的加载器,所以Java.xx用就不会有事,大家用就会有事。
想要使用Unsafe有三种艺术。一种是用反射,相比不难;此外一种是通过虚拟机运营参数-Xbootclasspath,把你的classpath变为运转路径之一,那样正是BootstarpClassLoader加载你的类,跟java.xx三个对待了,就不会报错了。能够见到,即便是足以调用,可是会有一步判断,判断是或不是中间会检查该CallerClass是还是不是由系统类加载器BootstrapClassLoader加载,因为它是不安全的类,官方api也未尝对这么些包下的类进行表达表明,倘诺是开发职员引用那么些包下的类则会抛错。由系统类加载器加载的类调用getClassLoader()会再次来到null,所以要检查类是还是不是为bootstrap加载器加载只供给检查该措施是还是不是回去null。
下边会重点讲解类加载器

4.3 类加载器

从底下的笺注大家能够只借使由bootstrap加载器加载的类,再次来到值是null,那也就越来越证实了,java官方禁止自定义使用该类。

 /**
     * Returns the class loader for the class.  Some implementations may use
     * null to represent the bootstrap class loader. This method will return
     * null in such implementations if this class was loaded by the bootstrap
     * class loader.
     *
     */
    @CallerSensitive
    public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;

        //JVM安全管理器,这里不做重点介绍
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }

    ClassLoader getClassLoader0() { return classLoader; }
4.3.1 Class 文件有哪些来源呢?

首先,最广泛的是开发者在应用程序中编辑的类,这么些类位居项目目录下;

接下来,有 Java 内部自带的 主旨类 如 java.lang、java.math、java.io 等
package 内部的类,位于 $JAVA_HOME/jre/lib/ 目录下,如
java.lang.String 类就是概念在 $JAVA_HOME/jre/lib/rt.jar 文件里;

除此以外,还有 Java 大旨扩展类,位于 $JAVA_HOME/jre/lib/ext
目录下。开发者也能够把团结编辑的类打包成 jar 文件放入该目录下;
说到底还有一种,是动态加载远程的 .class 文件。

既然如此有这样多类型的根源,那么在 Java 里,是由某贰个具体的 ClassLoader
来归并加载呢?如故由多少个 ClassLoader 来合作加载呢?

4.3.2 哪些 ClassLoader 负责加载上边几类 Class?

率先,我们来看级别最高的 Java 核心类 ,即$JAVA_HOME/jre/lib 里的中坚
jar 文件。那一个类是 Java 启动的功底类,由三个名为 BootstrapClassLoader
加载器负责加载,它也被称作
根加载器/教导加载器。注意,BootstrapClassLoader 比较卓越,它不一而再ClassLoader,而是由 JVM 内部贯彻;

然后,要求加载 Java 宗旨扩大类 ,即 $JAVA_HOME/jre/lib/ext 目录下的
jar 文件。这一个文件由 ExtensionClassLoader 负责加载,它也被称作
扩大类加载器。当然,用户只要把温馨开支的 jar 文件放在那一个目录,也会被
ExtClassLoader 加载;

接下去是开发者在类型中编辑的类,那些文件将由 AppClassLoader
加载器进行加载,它也被称作 系统类加载器 System ClassLoader;

说到底,假诺想远程加载如(本和姑件/互联网下载)的措施,则必须求和谐自定义一个ClassLoader,复写在那之中的 findClass() 方法才能得以贯彻。

因而能看到,Java 里提供了起码四类 ClassLoader 来分别加载差别来源的
Class。

4.3.4 解压查看$JAVA_HOME/jre/lib/rt.jar文件

import

isun\misc的Unsafe类

通过下边三个图,注解了,Unsafe类是由BootstrapClassLoader
加载器加载的,所以在获得classLoader时如常情况下是回去null。

4.3.5 CallerSensitive注解是怎么鬼?

周密的同校大概已经发现上边获得类加载器的措施上有该评释,那么它的效果是吗吧?大家先看stackoverflow网站给出的答案

CallerSensitive

简简单单,用@CallerSensitive评释修饰的办法从一起头就了然具体调用它的目的,那样就不要再经过一多元的检查才能分明具体调用它的对象了。它实在是调用sun.reflect.Reflection.getCallerClass方法。

4.4 说下AtomicInteger类的getAndIncrement方法

 public final int getAndIncrement() {

        // 当前值加1返回旧值,底层CAS操作
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int x) {
        int expected;
        do {
            //获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
            expected= this.getIntVolatile(o, offset);

        //第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,expected表示期望值,expected+x表示要设置的值。
        } while (!this.compareAndSwapInt(o, offset, expected, expected+ x));

        return expected;
    }

4.5 看线程的挂起与回复领会Unsafe运维机制

将1个线程进行挂起是通过park方法完毕的,调用
park后,线程将直接不通直到超时只怕暂停等条件现身。unpark能够告一段落一个挂起的线程,使其苏醒平常。Java对线程的挂起操作被封装在
LockSupport类中,LockSupport类中有种种本子pack方法,其底层完结最终依旧接纳Unsafe.park()方法和Unsafe.unpark()方法

//线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。  
public native void park(boolean isAbsolute, long time);  

//终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,其底层正是使用这两个方法,  
public native void unpark(Object thread); 

4.6 通过例子加深对Unsafe的知道

 private static Unsafe unsafe;

    public static void main(String[] args) {

        try {
            //通过反射获取rt.jar下的Unsafe类
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            // 设置该Field为可访问
            field.setAccessible(true);
            // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
            unsafe = (Unsafe) field.get(null);
            Integer target = 12;
            //compareAndSwapInt方法的属性分别是:目标对象实例,目标对象属性偏移量,当前预期值,要设的值.
            //compareAndSwapInt方法是通过反射修改对象的值,具体修改对象下面那个值,可以通过偏移量,对象字段的偏移量可以通过objectFieldOffset获取
            System.out.println(unsafe.compareAndSwapInt(target, 12, 12, 24));
        } catch (Exception e) {
            System.out.println("Get Unsafe instance occur error" + e);
        }
    }

输入不相同的参数得到以下结果:

不错的企盼值

谬误的只求值

4.7 CAS的ABA难题及其消除方案

CAS算法完成一个根本前提需求取出内部存款和储蓄器中某时刻的数目,而在下每日相比并替换(相比和置换是原子性),即使在取出和相比较并沟通之间产生多少变化而不可能发现,就涌出所谓的ABA难题了。

image.png

ABA难题造成的原由,是CAS进程中只不难举行了“值”的校验,再稍加意况下,“值”相同不会引入错误的政工逻辑(例如仓库储存),有个别情状下,“值”即便一样,却早已不是原本的数目了。

优化趋势:CAS不能够只比对“值”,还非得确认保证的是原先的数据,才能修改成功。

广泛实践:“版本号”的比对,3个多少2个版本,版本变化,即便值相同,也不应有修改成功。

五 、对volatile关键字的接头

5.1 volatile写操作的内部存款和储蓄器语义

当写贰个volatile变量时,JMM会把该线程对应的本土内部存款和储蓄器中的共享变量刷新到主内部存款和储蓄器

写操作

5.2 volatile读操作的内存语义

读操作

5.3 变量在内部存款和储蓄器中的工作历程

image.png

5.4 volatile非原子原因

  • 多线程环境下,”数据测算”和”数据赋值”操作可能数十遍并发,即操作非原子
  • 若数据在加载之后,若主内部存储器count变量产生修改之后,由于线程工作内部存款和储蓄器中的值在以前一度加载,从而不会对转移操作做出相应变更,即私有内部存款和储蓄器和国有内部存款和储蓄器中变量不一起,进而导致数据不雷同
  • 对于volatile变量,JVM只是保险从主内部存款和储蓄器加载到线程工作内部存款和储蓄器的值是最新的,也正是数据加载时是新型的。总而言之volatile化解的是变量读时的可知性难点,但不能保险原子性,对于十六线程修改共享变量的情景必须采用加锁同步

⑥ 、参考文章

偶尔的空子看了上面在那之中一篇文章便伊始对cas发生了感兴趣,激起作者延续看源码写文章的豪情。多谢上边的小编们,深度好文!

https://www.zybuluo.com/kiraSally/note/850631
http://www.10tiao.com/html/249/201706/2651960240/1.html
https://juejin.im/entry/595c599e6fb9a06bc6042514

相关文章