1、引例
引用上一篇JMM中的开篇代码,再次针对性分析。
/** * 验证volatile原子性 */ public class AtomicityTest implements Runnable { volatile int i = 0; @Override public void run() { synchronized (this){ i++;
} } public void value() { System.out.println("i=" + i); } public static void main(String[] args) { long start = System.currentTimeMillis(); AtomicityTest test = new AtomicityTest(); for (int j = 0; j < 10000; j++) { Thread t = new Thread(test); t.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } test.value(); long end = System.currentTimeMillis(); System.out.println("共耗时:"+(end-start)); } }
执行main方法,开启10000个并发线程,可以看到最后输出的结果不一定是10000,前面也讲到了i++不是一个原子操作,原因i++在底层实际分三步执行:1、从主内存拷贝变量副本到工作内存;2,执行自增操作;3,写回主内存,任何一阶段都存在被中断的风险,也就导致每个线程对i的操作不一定成功+1。而共享变量引入volatile修饰,前面也介绍到volatile只能保证共享变量改变时的可见性和有序性。那么怎样保证原子性呢,想到自然是synchronized加锁,然而sync锁机制会引起以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
那么既然sync会引起性能的问题,有没有好点的方式去解决这个既能保证原子性又能解决sync引起的性能问题呢?Java1.5并发包原子操作类(Atomic开头)解决了这个问题。看以下代码:
/** * Atomic原子操作类 */ public class AtomicTest implements Runnable { private static AtomicInteger atc = new AtomicInteger(0); @Override public void run() { atc.incrementAndGet(); } public static void main(String[] args) { long start = System.currentTimeMillis(); AtomicTest at = new AtomicTest(); for (int j = 0; j < 10000; j++) { Thread t = new Thread(at); t.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("i="+atc.get()); long end = System.currentTimeMillis(); System.out.println("共耗时:"+(end-start)); } }
AtomicInteger原子类中incrementAndGet()这个方法为什么能具有原子操作,顺着源码进去看看:
看到compareAndSwapInt了,这就是CAS,好吧,这正是今天 的主角。说到CAS先讲下两个锁:悲观锁和乐观锁
2、悲观锁和乐观锁
- 悲观锁:具有强烈的独占和排他特性,悲观锁总是假设最坏的情况,每次取数据时都会认为其他线程会修改。
- 乐观锁:相对悲观锁而言,乐观锁总是假定最好的情况,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制。
sync也是一种独占锁,而独占锁正是悲观锁的表现。独占锁,如读锁,写锁,数据库的行级锁和表级锁。它会引起其他需要同一把锁的线程挂起,等待当前持有锁资源的线程释放。
3、CAS
3.1 什么是CAS?
CAS,Compare And Swap,释义比较并替换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。CAS也称为自旋锁,在一个(死)循环【for(;;)】里不断进行CAS操作,直到成功为止(自旋操作),实际上,CAS也是一种乐观锁。
3.2 CAS目的
前面也讲到CAS是为解决非原子操作引起的并发安全问题(解决原子操作问题),同时优化性能(提高性能)而产生,CAS的原子操作是由CPU在指令级别上进行保证。
3.3 CAS实现原子操作的三大问题
- ABA问题:其他线程把共享变量从A改成了B,很快又改回了A,CAS检查时看起来没有变化,实际上产生了变化。解决这个问题使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。同时,Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
- 循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。可以看到看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。如:AtomicReference<User> ar = new AtomicReference<>();