本片博文 参考 “深入理解CAS算法原理” ,更多细节请查阅原博客。 添加一些个人理解,如果理解有误,请提出宝贵意见
1. Java中锁的基本介绍
对象和类的锁
- 只要是共享区域,就需要处理同步问题,下边是基本流程
- 虚拟机给每个对象和类都分配一个锁
- 同一时刻,只有一个线程可以拥有 这个类和对象
- 如果一个线程想要获得某个类或者对象的锁,需要询问虚拟机
- 当线程不再需要锁时,他再把锁还给虚拟机,这样虚拟机可以把锁再分配给其他申请锁的线程
- 对象锁和类锁
- 类锁其实通过对象锁实现的,当虚拟机加载一个类的时候,会会为这个类实例化一个
java.lang.Class
对象 - 锁类时,实际上是锁住的Class对象
- 类锁其实通过对象锁实现的,当虚拟机加载一个类的时候,会会为这个类实例化一个
1.2 监视器 (Monitors)
- 监视器和锁同时被JVM使用,监视器主要功能是监控一段代码,确保在同一时间只有一个线程执行
- 每个监视器都与一个对象相关联,
- 当线程执行到监视器监视下的代码块中第一条指令,线程必须获取对被引用对象锁定
- 获取锁之前,无法执行这段代码
1.3多次加锁
- 同一个线程可以对同一个对象进行多次加锁,
- 每个对象维护着一个记录着被锁次数的计数器 : 同步代码块
- 未被锁定的对象的该计数器为0
- 当一个线程获得锁后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增
- 当同一个线程释放锁的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁
1.4 同步
-
Java语言提供了两种内置方式来使线程同步的访问数据:同步代码块和同步方法
-
synchronized 可以对代码块和对象加锁
synchronized (this) 对代码块加锁,同一时间只有一个线程进入该段代码,其他线程类的线程不受影响
synchronized(objA) 对objA对象加锁,同一时间只有一个获得该锁的线程进入代码,其他的线程均处于阻塞状态,比如妻子消费者和儿子消费者都对家庭加了锁,那么同一时间只有一个人可以进行消费
2 . 乐观锁和CAS
-
Java中乐观锁和悲观锁
JDK5之前,java用synchronized锁来控制同步问题 ; JDK5之后,增加了并发包java.util.concurrent.* , 其中的一些原子操作类(Atomic开头)采用的CAS算法就是一种乐观锁
-
什么是CAS
CAS就是 “Compare And Swap” , 先比较在交换 。 CAS是一种无锁算法 , 基本原理如下
CAS有三个操作数,内存值V、旧的预期值、要修改的新值B。当且仅当 预期值A和内存值V相同时,将内存值V修改为B
-
CAS的理解
CAS比较交换的伪码可以表示为下
do{ 备份旧数据; 基于旧数据构造新数据; }while(!CAS(内存地址,备份的旧数据,新数据))
简单举例 :
设存在两个线程 t1 、t2 , 同时去修改内存中的 一个变量56 ; 按照线程执行的规则,它们分别把56copy到自己的工作内存,修改,然后刷新回主存; 设t1 将值修改为57,并写回到主存中,对于t2来说,预期值是56但是内存中值成了57,内存值与预期值不一样,修改失败
通俗解释:
内存值和预期值进行比较时,如果相等就表示共享数据没有被修改,就可以替换新值继续向下运行,如果不相等,那说明共享数据已经被修改了,就放弃已经做得操作,然后重新执行刚才的操作
CAS 操作是基于共享数据不会被修改的假设,当同步冲突出现机会很少时,这种假设会带来很大的性能提升
-
CAS开销
CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了
Cache Miss:
我们知道,CPU的硬件结构中 ,多喝处理器 存在多个CPU, 每个CPU又会存在缓存,管芯内还带有一个互联模块,使管芯内的两个核可以互相通信,数据以“缓存线”为单位在系统中传输。如下图所示
[外链图片转存失败(img-CwqMvhJ3-1567494517501)(…/img/17.png)]
CAS算法出现cache miss:
设 CPU0 在对一个变量执行“比较并交换”(CAS)操作,而该变量所在的缓存线在 CPU7 的高速缓存中,就会发生以下经过简化的事件序列
-
CPU0 检查本地高速缓存,没有找到缓存线
-
请求被转发到 CPU0 和 CPU1 的互联模块(interconnect),检查 CPU1 的本地高速缓存,没有找到缓存线。
-
请求被转发到系统互联模块(System Interconnect),检查其他三个管芯,得知缓存线被 CPU6和 CPU7 所在的管芯持有
-
请求被转发到 CPU6 和 CPU7 的互联模块,检查这两个 CPU 的高速缓存,在 CPU7 的高速缓存中找到缓存线
-
CPU7 将缓存线发送给所属的互联模块,并且刷新自己高速缓存中的缓存线
-
CPU6 和 CPU7 的互联模块将缓存线发送给系统互联模块
-
系统互联模块将缓存线发送给 CPU0 和 CPU1 的互联模块
-
CPU0 和 CPU1 的互联模块将缓存线发送给 CPU0 的高速缓存
-
CPU0 现在可以对高速缓存中的变量执行 CAS 操作
CAS开销:
最好的情况下(对某一个变量执行 CAS 操作的 CPU 正好是最后一个操作该变量的CPU,所以对应的缓存线已经在 CPU 的高速缓存中了), 消耗大约40纳秒 。最好情况下锁操作(一个“round trip 对”包括获取锁和随后的释放锁)消耗超过 60 纳秒,超过 100 个时钟周期。这里的“最好情况”意味着用于表示锁的数据结构已经在获取和释放锁的 CPU 所属的高速缓存中了
锁操作比 CAS 操作更加耗时,因为锁操作的数据结构中需要两个原子操作
-
-
CAS算法应用:java原子类(automicXXX)
Java1.7中 AtomicInteger.incrementAndGet()的实现源码为:
AtomicInteger.incrementAndGet的实现用了乐观锁技术,调用了类sun.misc.Unsafe库里面的 CAS算法,用CPU指令来实现无锁自增