之前的文章讲了ReentrantLock和synchronized都是通过锁来保证线程安全的,锁机制存在一些问题,例如:
? 在多线程的竞争下,加锁、释放锁会导致很多线程的上下文切换和调度,对性能有一定的影响;
? 一个线程持有锁会导致其他需要此锁的线程挂起(强行在锁的区域将并行变为串行);
? 使用不当还会导致死锁、饥饿、活锁等;
也许你会说,也可以用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操作。
原文地址:https://www.cnblogs.com/Joe-Go/p/9771452.html