CAS和Synchronized知识

x33g5p2x  于2022-02-14 转载在 其他  
字(2.1k)|赞(0)|评价(0)|浏览(193)

一. CAS

何为CAS。

CAS(Compare And Swap )是乐观锁的一种实现方式,是一种轻量级锁。JAVA1.5开始引入了CAS,JUC下很多工具类都是基于CAS。

CAS的实现方式

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。当多个线程同时尝试使用CAS更新一个变量时,任何时候只有一个线程可以更新成功,若更新失败,线程会重新进入循环再次进行尝试。

CAS在Java中的应用

前面也说了JUC下面很多工具类都用到了CAS。其主要依赖于Unsafe的CAS操作来进行实现。
例如AtomicInteger下的incrementAndGet操作:

接着来看看Unsafe下的getAndAddInt方法

可以看到Unsafe中在循环体内先读取内存中的value值,然后CAS更新,如果CAS更新成功则退出,如果更新失败,则循环重试直到更新成功。

CAS带来的问题

ABA问题

例如说:
一. 线程1查询值是否为A
二. 线程2查询值是否为A
三. 线程2使用CAS将值更新为B
四. 线程2查询值是否为B
五. 线程2使用CAS将值更新为A
六. 线程1使用CAS将值更新为C
线程一线程二交替执行。第二步到第五步,线程二将值由A更新为B由更新为A,但线程一并没有察觉,因此线程一还是可以继续执行。我们称这种现象为ABA问题。
解决方法:
使用版本号 (时间戳),在每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号加一,否则就执行失败。例如AtomicStampedReference就是通过对值加一个戳(stamp)来解决“ABA”问题的。

循环开销过大

CAS操作不成功的话,会导致一直自旋,CPU的压力会很大。例如说Unsafe下的getAndAddInt方法会一直循环,直到成功才会返回。

只能保证一个变量的原子操作

二. synchronized

相比于CAS基于乐观锁实现,synchronized是基于悲观锁的,当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

对于普通同步方法加锁时,锁是当前实例对象。

对于静态同步方法加锁时,锁是当前类的Class对象。

对于同步方法块加锁时,锁是Synchonized括号里配置的对象。

Synchronized的实现方式:
Synchonized是基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。Synchronized 用在方法上时,在字节码中是通过方法的 ACC_SYNCHRONIZED 标志来实现的。而代码块同步则是使用monitorenter和monitorexit指令实现的。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁,当获得对象的monitor以后,monitor内部的计数器就会自增(初始为0),当同一个线程再次获得monitor的时候,计数器会再次自增。当同一个线程执行monitorexit指令的时候,计数器会进行自减,当计数器为0的时候,monitor就会被释放,其他线程便可以获得monitor。

Synchronized的优化
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

偏向锁
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

轻量级锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

重量级锁
重量级锁是依赖对象内部的monitor锁来实现。当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,需要从用户态转换到内核态,而转换状态是需要消耗很多时间。

相关文章