CAS(Compare And Swap) ReentrantLock synchronized volatile

之前的文章讲了ReentrantLocksynchronized都是通过锁来保证线程安全的,锁机制存在一些问题,例如:

  ❤ 在多线程的竞争下,加锁、释放锁会导致很多线程的上下文切换和调度,对性能有一定的影响;

  ❤ 一个线程持有锁会导致其他需要此锁的线程挂起(强行在锁的区域将并行变为串行);

  ❤ 使用不当还会导致死锁、饥饿、活锁等;

也许你会说,也可以用volatile,volatile是轻量级的锁,但是不能保证原子性,所以最后还是会回到锁的机制上来;

synchronized和ReentrantLock都是独占锁,独占锁是一种悲观锁,会导致其余需要锁的线程挂起,等待持有锁的线程释放锁的资源,并且每次只能有一个线程执行;而另一个更加有效的锁就是乐观锁,乐观锁就是乐观的认为每次操作都没有冲突,如果有,则重试,直到成功为止,乐观锁使用到的机制就是CAS。

CAS(Compare And Swap)

CAS:Compare and Swap 即:比较并交换。设计并发计算时常用到的一种技术,java.util.concurrent包的基础建立在CAS之上,没有CAS就没有此包,可见CAS的重要性。

在Java语言出现之前,并发就已经广泛存在并在服务器领域得到了广泛的应用。所以硬件厂商很早就在芯片中加入了大量支持并发操作的原语,从而使硬件性能得到提升,在Intel中,采用的是cmpxchg指令。

在Java的发展初期,Java语言是不能利用硬件提供的这些便利来提升系统的性能的,随着Java的发展,JNI(Java Native Interface)的出现,使得Java程序可以越过JVM直接调用本地(本机)上的一些方法,这样不仅使得Java在并发上的手段增多了,同时也提高了Java系统的性能。

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

CAS是通过Unsafe类来实现的,下面看看Unsafe里的方法:

public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);

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

public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);

 上面这三种方法都是Native本地方法,而且都是原子操作(硬件保证)。第一个参数表示需要更新的对象,第二个long表示的是该对象在内存中的偏移地址,第三个是预期值(即旧值),第四个是新值。

下面我们通过AtomicInteger来增加对CAS的理解,先来看AtomicInteger的类的变量定义,源码如下:

 1 private static final Unsafe unsafe = Unsafe.getUnsafe();
 2     private static final long valueOffset;
 3 
 4     static {
 5       try {
 6         valueOffset = unsafe.objectFieldOffset
 7             (AtomicInteger.class.getDeclaredField("value"));
 8       } catch (Exception ex) { throw new Error(ex); }
 9     }
10 
11     private volatile int value;

上述代码解释:

  ❤ Unsafe是CAS的核心。

  ❤ valueOffset是变量在内存中的偏移地址,因为Unsafe就是根据偏移地址去获取数据的原值的;

  ❤ value是volatile关键字修饰的,这个非常重要,这保证了变量在线程中的可见性;

再来看一个方法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);
    }

incrementAndGet()该方法每次增加1,并返回增加后的值。相当于++i的功能,但是++i不是原子性的,incrementAndGet()是原子性的。

从上述代码可以看出,incrementAndGet()方法内是一个死循环,保证了value一定会+1成功并返回,利用unsafe.compareAndSwapInt(this, valueOffset, expect, update)保证了对于value修改的线程安全性。

CAS的缺点

  1.存在ABA情况。CAS在操作时,需要比较值有没有发生变化,没有变化则更新,如果一个变量由A变成了B,再由B变回了A,那么CAS在检查时,就是发现该变量的值没有发生改变,但是实际上却变化了。从Java1.5开始。JDK在atomic提供了一个AtomicStampedReference类来解决ABA问题这个类的compareAndSet(expectedReference, newReference,expectedStamp, newStamp)方法,会首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子的方式将该引用和该标志的值修改为给定的新值。

  2.循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

  3.只能保证一个共享变量的原子操作。当对一个共享变量进行操作时,我们可以采用CAS方式来保证变量的线程安全,当有多个变量时,CAS就无法保证操作的原子性了,这个时候就只能用锁或者采用AtomicReference类来保证引用对象之间的原子性,AtomicReference支持把多个变量放到一个对象里进行CAS操作。

猜你喜欢

转载自www.cnblogs.com/Joe-Go/p/9771452.html