zoukankan      html  css  js  c++  java
  • AtomicInteger

    AtomicInteger是一个原子操作的Integer的线程安全的类,用来取代Integer在多线程中的使用。

    那么AtomicInteger是怎么实现线程安全的呢?

    通过查看AtomicInteger的源码可知, 

    private volatile int value;
    
    public final boolean compareAndSet(int expect, int update) { 
      return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
    } 

    通过申明一个volatile (保证value修改后,在其他线程中可见)类型的变量,再加上unsafe.compareAndSwapInt的方法,来保证实现线程同步的。

    CAS指令在Intel CPU上称为CMPXCHG指令,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。这一比较并交换的操作是原子的,不可以被中断。初一看,CAS也包含了读取、比较 (这也是种操作)和写入这三个操作,和之前的i++并没有太大区别,是的,的确在操作上没有区别,但CAS是通过硬件命令保证了原子性,而i++没有,且硬件级别的原子性比i++这样高级语言的软件级别的运行速度要快地多。虽然CAS也包含了多个操作,但其的运算是固定的(就是个比较),这样的锁定性能开销很小。

    从内存领域来说这是乐观锁,因为它在对共享变量更新之前会先比较当前值是否与更新前的值一致,如果是,则更新,如果不是,则无限循环执行(称为自旋),直到当前值与更新前的值一致为止,才执行更新。

      简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。下面来看一下AtomicInteger是如何利用CAS实现原子性操作的。

    JDK1.7中,AtomicInteger中的incrementAndGet方法的内部实现为:

    public final int incrementAndGet() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return next;  
        }  
    }  
    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  

    其中,compareAndSwapInt()方法是一个navtive方法,

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);  

    第一个参数var1为给定的对象,var2为对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),var4表示期望值,var5表示要设置的值。如果指定的字段的值等于var4,那么就会把它设置为var5.

    循环内,获取当前值并设置更新值,调用compareAndSet进行CAS操作,如果成功就返回更新至,否则重试到成功为止。这里可能存在一个隐患,那就是循环时间过长,总是在当前线程compareAndSet时,有另一个线程设置了value(点子太背了),这个当然是属于小概率时间,目前Java貌似还不能处理这种情况。

    在JDK1.8中,AtomicInteger中的incrementAndGet方法的内部实现为:

    public final int incrementAndGet() {  
            return unsafe.getAndAddInt(this, valueOffset, 1) + 1;  
        } 

    可以看出,在jdk1.8中,直接使用了Unsafe的getAndAddInt方法,而在jdk1.7的Unsafe中,没有此方法。基本可以断定,Unsafe新增的方法是性能提升的关键。

    Unsafe中封装了一些类似指针的操作,因为指针的操作是不安全的。

    接下来,我们观察Unsafe的源码去看这新增的方法的实现原理:

    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;  
        } 
    public final int getAndSetInt(Object var1, long var2, int var4) {  
            int var5;  
            do {  
                var5 = this.getIntVolatile(var1, var2);  
            } while(!this.compareAndSwapInt(var1, var2, var5, var4));  
      
            return var5;  
        }  

    但是,Unsafe类是一个JDK内部使用的专属类,我们自己的应用程序无法直接使用Unsafe类。通过观察源码,可以知道获得Unsafe实例的方法是调动其工厂方法getUnsafe(),但是它的实现却是这样的:

    @CallerSensitive  
        public static Unsafe getUnsafe() {  
            Class var0 = Reflection.getCallerClass();  
            <strong>if (!VM.isSystemDomainLoader(var0.getClassLoader())) {  
                throw new SecurityException("Unsafe");</strong>  
            } else {  
                return theUnsafe;  
            }  
        }  

    可以看到它会检查调用getUnsafe()函数的类,判断调用这的类加载器是否是系统类加载器(系统类加载器为null),如果不是就直接抛出异常,拒绝工作。

    CAS 的缺点
    基本上 Java 的 concurrent 包都是建立在 CAS 的基础上的, 甚至还包括业界一个很出名的应用于高频交易的框架 Disruptor 也是利用 CAS 来保证原子性。 但是 CAS 还是有它的缺点:
     
    1.  ABA问题。因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
    从Java1.5开始JDK的atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
     
    2. 循环时间长开销大。自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果JVM能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation:内存顺序冲突一般是由伪/假共享引起,假共享是指多个 CPU 同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。
     
    3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
  • 相关阅读:
    back-不忘初心,方得始终。讲讲我主场3个月的经历。题外话。
    js中event事件对象的兼容问题以及坐标属性-(clientX clientY;pageX,pageY,offsetX,offsetY)
    布局(左边的div随着右边div的高度变化而变化)
    事件委托如何实现的原理
    寄生组合式继承方法的实现以及原理总结
    封装判断一个字符的后缀名和前缀的方法
    经典的面试题如果不通过其他任何变量实现两个数值类型的变量互相更换值。
    【十次方基础教程(后台)】Dockerfile脚本完成镜像的构建
    【十次方基础教程(后台)】安装并启动RabbitMQ
    【十次方基础教程(后台)】docker下安装head插件来进行Elasticsearch的操作
  • 原文地址:https://www.cnblogs.com/lostyears/p/9103817.html
Copyright © 2011-2022 走看看