AtomicInteger//volatile //cas原理以及用法

来源:互联网 发布:mpv播放器 mac 编辑:程序博客网 时间:2024/06/10 18:49

CAS原理: 
      通过查看AtomicInteger的源码可知, 
       `private volatile int value;

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

            通过申明一个volatile (内存锁定,同一时刻只有一个线程可以修改内存值)类型的变量,再加上unsafe.compareAndSwapInt的方法,来保证实现线程同步的。


二、CAS(Compare and Swap)

CAS指令在Intel CPU上称为CMPXCHG指令,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。这一比较并交换的操作是原子的,不可以被中断。初一看,CAS也包含了读取、比较 (这也是种操作)和写入这三个操作,但CAS是通过硬件命令保证了原子性,虽然CAS也包含了多个操作,但其的运算是固定的(就是个比较),这样的锁定性能开销很小。(J++的方式在java里并不是原子操作)

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

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


volatile变量

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. private volatile int value;  
首先声明了一个volatile变量value,我们知道volatile保证了变量的内存可见性,也就是所有工作线程中同一时刻都可以得到一致的值。

一般变量是先存在本地内存中,而加上volatile的意味着告诉jvm这是个不稳定变量,每次读都需要在主内存里操作

volatile典型用法(线程之间,对变量变化立刻感知)

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保他们所引用对象的状态的可见性,以及标示一些重要的程序声明周期事件的发生,如初始化或关闭。

 

?
1
2
3
4
volatileboolean asleep;
...
   while(!asleep)
    countSomeSheep();

上面是volatile变量的一种典型用法:检查某个状态标记以判断是否退出循环。在这个示例中,线程试
图通过类似于数数的传统方法进入休眠状态。为了使这个示例能正确执行,asleep必须为volatile变量。否则,当asleep被另一个线程修改时,执行
判断的线程却发现不了。我们也可以用锁来确保asleep更新操作的可见性,但是这样将使代码变得更加复杂。
虽然volatile很方便,但也存在一些局限性。volatile变量通常用作某个操作完成发生中断的标志。尽
管volatile变量也可以用于表示其他的状态信息,但在使用时要非常小心。例如volatile的语义不足以
确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量。

a,对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
b,该变量不会与其它状态变量一起纳入不变形条件中。
c,在访问变量时不需要加锁。



[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public final int get() {    
  2.     return value;    
  3. }  

Compare And Set

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. // setup to use Unsafe.compareAndSwapInt for updates    
  2. private static final Unsafe unsafe = Unsafe.getUnsafe();    
  3. private static final long valueOffset;// 注意是静态的    
  4.     
  5. static {    
  6.   try {    
  7.     valueOffset = unsafe.objectFieldOffset    
  8.         (AtomicInteger.class.getDeclaredField("value"));// 反射出value属性,获取其在内存中的位置    
  9.   } catch (Exception ex) { throw new Error(ex); }    
  10. }    
  11.     
  12. public final boolean compareAndSet(int expect, int update) {    
  13.   return unsafe.compareAndSwapInt(this, valueOffset, expect, update);    
  14. }    

循环设置

       现在在来看开篇提到的两个方法,我们拿incrementAndGet来分析一下其实现过程。
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public final int incrementAndGet() {    
  2.     for (;;) {// 这样优于while(true)    
  3.         int current = get();// 获取当前值    
  4.         int next = current + 1;// 设置更新值    
  5.         if (compareAndSet(current, next))    
  6.             return next;    
  7.     }    
  8. }   
循环内,获取当前值并设置更新值,调用compareAndSet进行CAS操作,如果成功就返回更新至,否则重试到成功为止。

cas缺点

       虽然使用CAS可以实现非阻塞式的原子性操作,但是会产生ABA问题,关于ABA问题:

       有ABA问题(即在更新前的值是A,但在操作过程中被其他线程更新为B,又更新为 A),这时当前线程认为是可以执行的,其实是发生了不一致现象,如果这种不一致对程序有影响(真正有这种影响的场景很少,除非是在变量操作过程中以此变量为标识位做一些其他的事,比如初始化配置),则需要使用AtomicStampedReference(除了对更新前的原值进行比较,也需要用更新前的 stamp标志位来进行比较)。

总结: 
可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。不推荐在复杂操作中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题

0 0
原创粉丝点击