Java中的CAS和自旋锁详解
作者:不会叫的狼
什么是CAS
CAS算法(Compare And Swap),即比较并替换,是一种实现并发编程时常用到的算法,Java并发包中的很多类都使用了CAS算法。
CAS算法有3个基本操作数:
- 内存地址V
- 旧的预期值A
- 要修改的新值B
CAS使用自旋的方式来交换值,操作步骤为:
- 读取内存地址V的值保存在A中
- 在原子操作中比较内存地址V的值是否与A相同
- 相同时,修改内存地址V的值为B,原子操作成功。
- 不相同时,循环执行第一至第三步(自旋),直到成功。
什么是自旋锁?
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。
对于互斥锁,会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。但是自旋锁不会引起调用者堵塞,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。
自旋锁的实现基础是CAS算法机制。CAS自旋锁属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
基于CAS实现-原子操作类
先看一个线程不安全的例子:
public class AutomicDemo { public static int num = 0; public static void main(String[] args){ for(int i=0; i<5; i++){ new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } for(int j=0; j<200; j++){ num++; } } }).start(); } /* 主控失眠3秒,保证所有线程执行完成 */ try { Thread.sleep(15000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("num=" + num); } }
输出结果:
num=950
因为自增操作不是原子性,多线程环境下,访问共享变量线程不安全。
解决方法,加synchronized同步锁:
for(int j=0; j<200; j++){ synchronized (AutomicDemo.class){ num++; } }
输出结果:
num=1000
线程安全。
synchronized确保了线程安全,但会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
于是JDK提供了一系列原子操作类:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等,它们都是基于CAS去实现的,下面我们就来详细看一看原子操作类。
public class AutomicDemo { public static AtomicInteger num = new AtomicInteger(0); public static void main(String[] args){ for(int i=0; i<5; i++){ new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } for(int j=0; j<200; j++){ num.incrementAndGet(); } } }).start(); } /* 主控失眠3秒,保证所有线程执行完成 */ try { Thread.sleep(15000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("num=" + num.get()); } }
输出:
num=1000
使用AtomicInteger之后,最终的输出结果同样可以保证是200。并且在某些情况下,代码的性能会比Synchronized更好。
原子类操作方法
- public final int get():取得当前值
- public final void set(int newValue):设置当前值
- public final int getAndSet(int newValue):设置新值并返回旧值
- public final boolean compareAndSet(int expect, int update):如果当前值为expect,则设置为update
- public final boolean weakCompareAndSet(int expect, int update):如果当前值为expect,则设置为update,可能失败,不提供保障
- public final int getAndIncrement():当前值加1,返回旧值
- public final int getAndDecrement():当前值减1,返回旧值
- public final int getAndAdd(int delta):当前值加delta,返回旧值
- public final int incrementAndGet():当前值加1,返回新值
- public final int decrementAndGet():当前值减1,返回新值
- public final int addAndGet(int delta):当前值加delta,返回新值
AtomicInteger底层原理
所有Atomic相关类的实现都是通过CAS(Compare And Swap)去实现的,它是一种乐观锁的实现。
CAS实现放在 Unsafe 这个类中的,其内部大部分是native方法。这个类是不允许更改的,而且也不建议开发者调用,它只是用于JDK内部调用,看名字就知道它是不安全的,因为它是直接操作内存,稍不注意就可能把内存写崩。
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力。有一下特点:
1、不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏。
2、Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
3、直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。
去看AtomicInteger的内部实现可以发现,全是调用的Unsafe类中的方法:
CAS机制ABA问题
ABA问题:CAS算法通过比较变量值是否相同来修改变量值以保证原子性,但如果一个内存地址的值本身是A,线程1准备修改为C。在这期间,线程2将值修改为B,线程3将值修改为A,线程1获取内存地址的值还是A,故修改为C成功。但获取的A已不再是最开始那一个A。这就是经典的ABA问题,A已不再是A。
如何解决ABA问题呢?
两个方法,1、增加版本号;2、增加时间戳。
- 增加版本号:让值的修改从A-B-A-C变为1A-2B-3A-4C;这样在线程1 中就能判别出1A不是当前内存中的3A,从而不会更新变量为4C。
- 增加时间戳:值被修改时,除了更新数据本身外,还必须更新时间戳。对象值以及时间戳都必须满足期望,写入才会成功。JDK提供了一个带有时间戳的CAS操作类AtomicStampedeReference。
CAS的缺点
Java原子类使用自旋的方式来处理每次比较不相同时后的重试操作,下面来看看AtomicInteger类incrementAndGet方法的代码:
//AtomicInteger 的incrementAndGet方法,变量U为静态常量jdk.internal.misc.Unsafe类型 public final int incrementAndGet() { //使用getAndAddInt方法,实际操作类似j++ return U.getAndAddInt(this, VALUE, 1) + 1; } //jdk.internal.misc.Unsafe类型的getAndAddInt方法 public final int getAndAddInt(Object o, long offset, int delta) { int v; do { //获取变量o的可见值 v = getIntVolatile(o, offset); //比较与替换变量o(CAS算法) } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; } //jdk.internal.misc.Unsafe类型的weakCompareAndSetInt方法 public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) { //执行比较与替换 return compareAndSetInt(o, offset, expected, x); }
在Unsafe类的getAndAddInt方法中使用了do…while循环语句,循环语句的作用为自旋,Unsafe的weakCompareAndSetInt实现CAS算法。
如果weakCompareAndSetInt一直不成功将会一直自旋下去,这将消耗过多的CPU时间。而且原子类使用CAS算法实现,这导致原子类只能保证一个变量的原子操作,对于需要保证一个具有多个操作的事务将变得无能为力。
总结如下:
- CPU开销较大
- 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,自旋会给CPU带来很大的压力。
- 不能保证代码块的原子性
- CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
为应对CAS存在缺点,替换方案如下:
- 自旋效率:Java提供了自适应自旋锁
- 片面性:应对片面性问题Java提供了读写锁
- ABA问题:用AtomicStampedReference/AtomicMarkableReference解决ABA问题
到此这篇关于Java中的CAS和自旋锁详解的文章就介绍到这了,更多相关Java的CAS和自旋锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!