资讯专栏INFORMATION COLUMN

浅谈cas

琛h。 / 1269人阅读

摘要:在的包中,大神大量使用此技术,实现了多线程的安全性。我们将变量用修饰,保证线程间的可见性。线程也通过此方法获取当前值,进行操作,比较内存值相等进行修改。我们通过保证了对的并发线程安全,其安全的保证是通过调用的代码实现的。

前言

研究java并发编程有一段时间了, 在并发编程中cas出现的次数极为频繁。cas的英文全名叫做compare and swap,意思很简单就是比较并交换。在jdk的conurrent包中,Doug Lea大神大量使用此技术,实现了多线程的安全性。
cas的核心思想就是获取当前的内存偏移值、期望值和更新值,如果根据内存偏移值得到的变量等于期望值,则进行更新。

问题

总有面试官喜欢问你i++和++i,以及经典的字符串问题,其实这些问题只要你试用javap -c这个命令反编译一下,就一目了然。当然今天的主题是cas,我首先来研究下a++:

//@RunWith(SpringRunner.class)
//@SpringBootTest
public class SblearnApplicationTests {

    public static volatile  int a;
    public static void main(String[] args) {
        a++;
    }

}

通过javac SblearnApplicationTests.java,javap -c SblearnApplicationTests.class可以得到:

Compiled from "SblearnApplicationTests.java"
public class com.example.sblearn.SblearnApplicationTests {
  public static volatile int a;

  public com.example.sblearn.SblearnApplicationTests();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field a:I
       3: iconst_1                          //当int取值-1~5采用iconst指令,取值-128~127采用bipush指令,取值-32768~32767采用sipush指令,取值-2147483648~2147483647采用 ldc 指令
       4: iadd
       5: putstatic     #2                  // Field a:I
       8: return
}

通过反编译得出如上的结果,都是一些jvm的指令,百度一下就能知道意思。我们将变量a用violate修饰,保证线程间的可见性。通过jvm指令可知a++不是一个原子动作,如果多个线程同事对a进行操作,无法保证线程安全,那怎么解决呢?

解决方案

java给我们提供了一个关键字synchronized,可以对成员方法、静态方法、代码块进行加锁,从而保证操作的原子性。但效率不高,还有其他办法吗?当然有了,就是我们今天的主角cas。接下来我们再来看看concurrent包下的AtomicInteger:

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

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    //效果等同于a++,但保证了原子性
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    }
public final class Unsafe {

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

    public native int getIntVolatile(Object var1, long var2);
    //object var1:当前AtomicInteger对象,long var2Integer对象的内存偏移值,int var4 增加的值
    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;
    }
    
    }

cas机制的核心类就是Unsafe,valueOffset 是其内存偏移值。由于java语言无法直接操作底层,需要本地方法(native method)来访问,unsafe这个类中存在大量本地方法,就是在调用c去操作特定内存的数据。我们先假设unsafe帮我们保证了原子性,先来分析下AtomicInteger.getAndIncrement(),在jdk1.8中,其实现就是Unsafe.getAndAddInt()

现在我们假设有A、B线程同时来操作AtomicInteger,其初始值为1,根据java内存模型,当前主内存AtomicInteger值为1,线程A、线程B各自的工作内存也为1.

线程A获得通过getIntVolatile获取当前值,被挂起。线程B也通过此方法获取当前值,进行操作,比较内存值相等进行修改。

这时线程A恢复,执行compareAndSwapInt发现与内存期望值不相等,重新获取var5变量(因为被violate修饰,所以工作内存和主内存变量一致),再次比较与内存期望值相等,进行更新。

我们通过cas保证了对value的并发线程安全,其安全的保证是CAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。而compareAndSwapInt就是借助C来调用CPU底层指令实现的。下面从分析比较常用的CPU(intel x86)来解释CAS的实现原理。compareAndSwapInt方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011openjdkhotspotsrcoscpuwindowsx86vm atomicwindowsx86.inline.hpp(对应于windows操作系统,X86处理器)。下面是对应于intel x86处理器的源代码的片段:

// Adding a lock prefix to an instruction on MP machine  
// VC++ doesn"t like the lock prefix to be on a single line  
// so we can"t insert a label after the lock prefix.  
// By emitting a lock prefix, we can define a label after it.  
#define LOCK_IF_MP(mp) __asm cmp mp, 0    
                       __asm je L0        
                       __asm _emit 0xF0   
                       __asm L0:  
  
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {  
  // alternative for InterlockedCompareExchange  
  int mp = os::is_MP();  
  __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 cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

intel的手册对lock前缀的说明如下:

1.确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
2.禁止该指令与之前和之后的读和写指令重排序。
3.把写缓冲区中的所有数据刷新到内存中。

cas的缺点

cas的缺点就是会出现aba问题,假如一个字母为a,它经历a->b->a的过程,实际已经改变两次,但值相同。部分业务场景是不允许出现这种情况的(比如银行转账..).解决办法就是添加版本号,他就变成了1a->2b>3a。jdk1.5之后也提供了AtomicStampedReference来解决aba问题。

总结

自旋cas如果长时间不成功,将会对cpu带来非常大的开销。cas只能保证一个共享变量的原子操作。所以非常简单的操作又不想引入锁,cas是一个非常好的选择。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68372.html

相关文章

  • 浅谈Java并发编程系列(七) —— 深入解析synchronized关键字

    摘要:第一个字被称为。经量级锁的加锁过程当一个对象被锁定时,被复制到当前尝试获取锁的线程的线程栈的锁记录空间被复制的官方称为。根据锁对象目前是否处于被锁定状态,撤销偏向后恢复到未锁定或经量级锁定状态。 Synchronized关键字 synchronized的锁机制的主要优势是Java语言内置的锁机制,因此,JVM可以自由的优化而不影响已存在的代码。 任何对象都拥有对象头这一数据结构来支持锁...

    piglei 评论0 收藏0
  • 浅谈java中的并发控制

    摘要:并发需要解决的问题功能性问题线程同步面临两个问题,想象下有两个线程在协作工作完成某项任务。锁可用于规定一个临界区,同一时间临界区内仅能由一个线程访问。并发的数据结构线程安全的容器,如等。 并发指在宏观上的同一时间内同时执行多个任务。为了满足这一需求,现代的操作系统都抽象出 线程 的概念,供上层应用使用。 这篇博文不打算详细展开分析,而是对java并发中的概念和工具做一个梳理。沿着并发模...

    Gilbertat 评论0 收藏0
  • Java并发核心浅谈

    摘要:耐心看完的你或多或少会有收获并发的核心就是包,而的核心是抽象队列同步器,简称,一些锁啊信号量啊循环屏障啊都是基于。 耐心看完的你或多或少会有收获! Java并发的核心就是 java.util.concurrent 包,而 j.u.c 的核心是AbstractQueuedSynchronizer抽象队列同步器,简称 AQS,一些锁啊!信号量啊!循环屏障啊!都是基于AQS。而 AQS 又是...

    cppowboy 评论0 收藏0
  • 浅谈Java中锁的实现和优化

    摘要:这两种策略的区别就在于,公平策略会让等待时间长的线程优先执行,非公平策略则是等待时间长的线程不一定会执行,存在一个抢占资源的问题。 之前有一篇文章我们简单的谈到了Java中同步的问题,但是可能在平常的开发中,有些理论甚至是某些方式是用不到的,但是从程序的角度看,这些理论思想我们可以运用到我们的开发中,比如是不是应该一谈到同步问题,就应该想到用synchronized?,什么时候应该用R...

    DevWiki 评论0 收藏0
  • Java并发核心浅谈(二)

    摘要:在线程处理任务期间,其它线程要么循环访问,要么一直阻塞等着线程唤醒,再不济就真的如我所说,放弃锁的竞争,去处理别的任务。写锁的话,独占写计数,排除一切其他线程。 回顾 在上一篇 Java并发核心浅谈 我们大概了解到了Lock和synchronized的共同点,再简单总结下: Lock主要是自定义一个 counter,从而利用CAS对其实现原子操作,而synchronized是c++...

    Null 评论0 收藏0

发表评论

0条评论

琛h。

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<