java中CAS算法保证原子性 无锁编程

悲观锁和乐观锁

  • 悲观锁会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
  • 乐观锁每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

synchronized的缺点:

在JDK1.5之前java语言是使用sychronized关键字实现同步,多线程并发。但是这样会有锁机制(对线程造成影响的,下面介绍的CAS其实也是有锁的,是硬件实现的锁),造成的问题如下:

  • 1.多线程下加锁和释放锁会导致比较多的上下文切换和调度延时,造成性能问题。
  • 2.sychronized的锁机制是独占锁,一个线程独占对象唯一的锁,其他要得到锁的线程就要被阻塞,处于对象锁阻塞状态。
  • 3.如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

CAS算法

CAS,英文全称是Compare and Swap,比较和交换,CAS算法属于乐观锁。在java.util.concurrent包中借助CAS实现了区别于synchronized悲观锁的一种乐观锁。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做

下面通过java.util.concurrent包下的AtomicInteger来看看CAS算法是怎样体现的。

<span style="font-size:18px;">/**
     * Atomically sets to the given value and returns the old value.
     *
     * @param newValue the new value
     * @return the previous value
     */
    public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }</span>
<span style="font-size:18px;">/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }</span>

下面是Unsafe类中的compareAndSwapInt方法

<span style="font-size:18px;">public final native boolean compareAndSwapInt(Object o, long offset,
                    int expected,
                    int x);</span>


是一个native方法,本地方法。

注意getAndSet方法中的循环,当执行CAS失败后继续进行循环直到成功为止,这样的方式并没有把线程阻塞,这样确实比synchronized独占锁方式性能高很多。

下面看看CAS的原理

CAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。

而compareAndSwapInt就是借助C来调用CPU底层指令实现的。下面从分析比较常用的CPU(intel x86)来解释CAS的实现原理。

Unsafe类中的那个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(对应于windows操作系统,X86处理器)。下面是对应于intel x86处理器的源代码的片段:

<span style="font-size:18px;">// 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
  }
}</span>


如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

CAS算法在硬件中的实现是cmpxchg指令,保证这个指令的原子性。执行这个指令时,会通过CPU锁来实现的原子性。java在1.5之后其实是提供了调用硬件cmpxchg指令的方法(Unsafe类中那个native CAS方法)而已。

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

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

下面来简单具体介绍一下CPU的锁:


总线锁:
如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。

处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。

缺点:保证了原子性,但是性能太差了,总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。

缓存锁:
所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。
所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。

这样也就是只是锁定了每个处理器的某一个缓存行(共享变量对应的缓存空间,如果这个处理器中有该变量的缓存),这样降低了锁的粒度,提高了性能。

总结

使用CAS可以提高性能,但是CAS同样也存在一些问题,比如说ABA的问题。使用CAS可以实现无“锁”编程。

    原文作者:java锁
    原文地址: https://blog.csdn.net/glory1234work2115/article/details/50822784
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注