AtomicInteger研究

来源:互联网 发布:最好的文档扫描软件 编辑:程序博客网 时间:2024/06/06 17:23

概念

AtomicInteger:java中无锁的线程安全整数,提供原子操作。由于++i和i++操作线程不安全,在使用的时候,难免会使用synchronized来保证线程安全。使用AtomicInteger可以通过一种线程安全的加减操作接口。

底层实现

通过硬件提供的原子操作指令实现。
关于处理器的原子操作,有如下三种加锁方式。
1:处理器自动保证基本内存操作的原子性
处理器保证从系统内存中读取或者写入一个字节是原子的。当一个处理器读取一个字节的时候,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同意缓存里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线所动和缓存锁定两个机制来保证复杂内存操作的原子性
2:使用总线锁保证原子性(开销较大)
如果多个处理器同时对共享变量进行读写改操作,各个处理器同时从各自的缓存中读取变量i,分别操作变量,然后存入系统内存当中。那么想要保证读写共享变量的操作是原子的,就必须保证多个cpu只能有一个能操作共享变量。这里就用到总线锁来解决,就是使用处理器提供的lock信号,当一个处理器在总线上输出信号时,其他处理器的请求将被阻塞,达到独占使用共享内存
3:使用缓存保证原子性
频繁使用的内存会缓存在处理器L1,L2,L3高速缓存里,那么原子操作就直接可在内部缓存中进行,并不需要声明总线锁,在奔腾6和最新的处理器里,可以使用缓存锁定的方式来实现原子性。就是如果缓存在处理器缓存行中内存区域在lock操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声明lock信号,而是修改内部的内存地址,并允许它的缓存一致性来保证操作原子性,因为缓存一致性会阻止同时被多个处理器修改的内存区域的数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。

但是,有以下两种情况下处理器不会使用缓存锁定,第一种:当操作的数据不能被缓存在处理器内部,或者操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况:有些处理器不支持缓存锁定。

AtomicInteger源码

public class AtomicInteger extends Number implements java.io.Serializable     private static final long serialVersionUID = 6214790243416807050L;        static {        try {            valueOffset = unsafe.objectFieldOffset                (AtomicInteger.class.getDeclaredField("value"));        } catch (Exception ex) { throw new Error(ex); }    }    private volatile int value;    public final int get() {        return value;    }

继承Number,方便类型转换,手动序列化ID,减小javac编译器开销。获取unsafe实例,volatile声明value实现可见性,底层实现是内存栅栏,保证每次取到都是最新值。final修饰,不可继承,进一步保证线程安全。
valueOffset表示的是变量值在内存中的偏移地址,因为unsafe就是根据内存的偏移地址获取值的。
自减操作:

  /**     * Atomically decrements by one the current value.     *     * @return the previous value     */    public final int getAndDecrement() {        return unsafe.getAndAddInt(this, valueOffset, -1);    }    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;    }

源码基于jdk1.8.
可以看到相对1.7CAS又一步增强。CAS性能不如synchronized,并不能简单地说明后者就更好,使用方式的差异和应用场景不同。

假设现在线程A和线程B同时执行getAndAdd操作:
AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。
线程A通过getIntVolatile(var1, var2)方法获取到value值3,线程切换,线程A挂起。
线程B通过getIntVolatile(var1, var2)方法获取到value值3,并利用compareAndSwapInt方法比较内存值也为3,比较成功,修改内存值为2,线程切换,线程B挂起。
线程A恢复,利用compareAndSwapInt方法比较,发手里的值3和内存值4不一致,此时value正在被另外一个线程修改,线程A不能修改value值。
线程的compareAndSwapInt实现,循环判断,重新获取value值,因为value是volatile变量,所以线程对它的修改,线程A总是能够看到。线程A继续利用compareAndSwapInt进行比较并替换,直到compareAndSwapInt修改成功返回true。
整个过程中,利用CAS保证了对于value的修改的线程安全性。
我们继续查看compareAndSwapInt代码:

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

可以看到,这是一个本地方法调用,这个本地方法在openjdk中依次调用c++代码:unsafe.cpp,atomic.cpp,atomic_window_x86.inline.hpp。下面是对应于intel X86处理器的源代码片段。

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {    int mp = os::isMP(); //判断是否是多处理器    _asm {        mov edx, dest        mov ecx, exchange_value        mov eax, compare_value        LOCK_IF_MP(mp)        cmpxchg dword ptr [edx], ecx    }}

从上面的源码中可以看出,会根据当前处理器类型来决定是否为cmpxchg指令添加lock前缀。

如果是多处理器,为cmpxchg指令添加lock前缀。
反之,就省略lock前缀。(单处理器会不需要lock前缀提供的内存屏障效果)
intel手册对lock前缀的说明如下:

确保对内存读改写操作的原子执行。
在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。
禁止该指令,与前面和后面的读写指令重排序。
把写缓冲区的所有数据刷新到内存中。
上面的第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。

CAS中ABA问题

CAS中一直都存在的一个问题,即当变量初次读取时是A,再次读取时,假如遇到另一个线程先改成了B再改回A,那么cas操作就会误认为没有被修改过.
尽管cas成功,但可能存在潜藏问题,如:
这里写图片描述
现在用一个单向链表实现堆栈,栈顶为A,这时线程T1已经知道a.next为B.然后希望用cas将栈顶替换为B:head.compareAndSet(A,B);
在T1执行这条指令之前,线程T2介入,将AB出栈,再PUSH D,C,A,此时的堆栈结构如下:而对象B此时处于游离状态:
这里写图片描述
此时轮到线程T1执行CAS操作,检测栈顶仍然为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时情况变为:
这里写图片描述
其中堆栈中只有一个B元素,cd不在堆栈中,平白无故就把cd丢掉了。

针对这种情况,java并发包提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过变量值版本号来保证CAS正确性。例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:

import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.atomic.AtomicStampedReference;public class ABA {        private static AtomicInteger atomicInt = new AtomicInteger(100);        private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);        public static void main(String[] args) throws InterruptedException {                Thread intT1 = new Thread(new Runnable() {                        @Override                        public void run() {                                atomicInt.compareAndSet(100, 101);                                atomicInt.compareAndSet(101, 100);                        }                });                Thread intT2 = new Thread(new Runnable() {                        @Override                        public void run() {                                try {                                        TimeUnit.SECONDS.sleep(1);                                } catch (InterruptedException e) {                                }                                boolean c3 = atomicInt.compareAndSet(100, 101);                                System.out.println(c3); // true                        }                });                intT1.start();                intT2.start();                intT1.join();                intT2.join();                Thread refT1 = new Thread(new Runnable() {                        @Override                        public void run() {                                try {                                        TimeUnit.SECONDS.sleep(1);                                } catch (InterruptedException e) {                                }                                atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);                                atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);                        }                });                Thread refT2 = new Thread(new Runnable() {                        @Override                        public void run() {                                int stamp = atomicStampedRef.getStamp();                                try {                                        TimeUnit.SECONDS.sleep(2);                                } catch (InterruptedException e) {                                }                                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);                                System.out.println(c3); // false                        }                });                refT1.start();                refT2.start();        }}
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 厌奶期宝宝瘦了怎么办 二个月的宝宝不喝夜奶怎么办 婴儿不喝奶粉怎么办 小孩整天不吃饭怎么办 婴儿不吃不喝怎么办 断奶后不吃奶瓶怎么办 小孩早上不吃饭怎么办 新生儿不认乳头怎么办 宝宝不吸奶嘴怎么办 孩子不会吸奶瓶怎么办 宝宝突然不吃奶瓶怎么办 换了奶瓶不喝奶怎么办 新生儿不喝奶粉怎么办 7个月小婴儿磨牙怎么办 宝宝出生四天不喝母乳怎么办 我的奶水不足怎么办 乳牙长得不整齐怎么办 新生儿只吃奶粉怎么办 小孩不肯吸母乳怎么办 三个月宝宝不吃奶粉怎么办 宝宝不爱喝水怎么办 崔玉涛 小孩身体铅过高怎么办 疫苗引起的发烧怎么办 婴儿不吃米糊怎么办 宝宝米糊不吃怎么办 换奶瓶宝宝不吃怎么办 小孩不会吃奶瓶怎么办 百天不吃奶瓶怎么办 1岁宝宝积食怎么办 宝宝退烧后流汗怎么办 宝宝高烧后出汗怎么办 发烧出汗不退烧怎么办 婴儿发烧不出汗怎么办 婴幼儿发烧不退怎么办 宝宝突然不吃饭怎么办 宝宝吃饭到处跑怎么办 宝宝不吃奶瓶怎么办崔玉涛 八个月母乳不足怎么办 八个月宝宝厌食怎么办 婴儿辅食便秘怎么办 婴儿被食物卡住怎么办