Java CAS底层实现原理和延伸思考

问题抛出

  1. CAS底层实现原理
  2. 高并发情况下存在ABA问题以及如何解决

延伸思考

  1. 和synchronized,lock的区别,以及synchronized锁优化的过程
  2. 高并发环境下用cas还是synchronized这是个坑

synchronized关键字保证同步的,这会导致有锁。

锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。


什么是CAS

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。


CAS底层原理

以AtomicInteger举例
要点:Unsafe类(存在rt.jar中)+CAS自旋锁
1、Unsafe类
是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,Unsafe 类是个跟底层硬件CPU指令通讯的复制工具类。

Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作执行依赖于Unsafe类。

Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。(和底层操作系统和Cpu有关系)底层用C++写的。

2.AtomicInteger中变量valueOffset,表示该变量在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

总结:getAndIncrement()底层调用unsafe类方法,传入三个参数,unsafe.getAndAddInt() 底层使用CAS思想,如果比较成功加1,如果比较失败重新获得,再比较一次,直至成功。


CAS缺点

  1. 循环时间长开销大(如果CAS失败,会一直尝试)
  2. 只能保证一个共享变量的原子操作。(对多个共享变量操作时,循环CAS无法保证操作的原子性,只能用加锁来保证)
  3. 存在ABA问题

什么是ABA问题

当第一个线程执行CAS(V,E,U)操作,在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样我们就无法正确判断这个变量是否已被修改过。


ABA问题的解决方案

AtomicStampedReference:是一个带有时间戳的对象引用,在每次修改后,不仅会设置新值还会记录更改的时间。
AtomicMarkableReference:维护的是一个boolean值的标识,这种方式并不能完全防止ABA问题的发生,只能减少ABA发生的概率。


延伸思考

我们把cas和synchronized还有lock做一个对比。
做对比前。我们先介绍下synchronized和lock

synchronized

适用场景:悲观认为并发很高,需要阻塞,需要上锁。
特点:语言层面的优化,锁粗化、偏向锁、轻量锁等等;可读性高。

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。

ReentrantLock

ReentrantLock的底层实现是AQS,而AQS底层实现是Volatile+CAS+CLH队列
可重入锁。在并发不高竞争不激烈时候,性能略低于synchronized;相反,并发高竞争激烈时候,性能高于synchronized。
可以通过构造方法,指定是公平锁还是非公平锁。

公平锁:会按照请求的顺序获取锁,如果锁已经被占用,则新来的请求放到队列中。
非公平锁:不是按照请求顺序获取锁,存在插队现象。

什么是AQS

AQS,即AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架。

AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。从图中可以看出,在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用。

抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch

它维护了一个volatile int state(代表共享资源)和一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列)

在这里插入图片描述
AQS的实现依赖内部的同步队列(FIFO双向队列),如果当前线程获取同步状态失败,AQS会将该线程以及等待状态等信息构造成一个Node,将其加入同步队列的尾部,同时阻塞当前线程,当同步状态释放时,唤醒队列的头节点。

上面说的有点抽象,来具体看下,首先来看AQS最主要的三个成员变量:

   private transient volatile Node head;
    
    private transient volatile Node tail;

    private volatile int state;

上面提到的同步状态就是这个int型的变量state. head和tail分别是同步队列的头结点和尾结点。假设state=0表示同步状态可用(如果用于锁,则表示锁可用),state=1表示同步状态已被占用(锁被占用)。

下面举例说下获取和释放同步状态的过程:

获取同步状态
假设线程A要获取同步状态(这里想象成锁,方便理解),初始状态下state=0,所以线程A可以顺利获取锁,A获取锁后将state置为1。在A没有释放锁期间,线程B也来获取锁,此时因为state=1,表示锁被占用,所以将B的线程信息和等待状态等信息构成出一个Node节点对象,放入同步队列,head和tail分别指向队列的头部和尾部(此时队列中有一个空的Node节点作为头点,head指向这个空节点,空Node的后继节点是B对应的Node节点,tail指向它),同时阻塞线程B(这里的阻塞使用的是LockSupport.park()方法)。后续如果再有线程要获取锁,都会加入队列尾部并阻塞。

释放同步状态
当线程A释放锁时,即将state置为0,此时A会唤醒头节点的后继节点(所谓唤醒,其实是调用LockSupport.unpark(B)方法),即B线程从LockSupport.park()方法返回,此时B发现state已经为0,所以B线程可以顺利获取锁,B获取锁后B的Node节点随之出队。

上面只是简单介绍了AQS获取和释放的大致过程,下面结合AQS和ReentrantLock源码来具体看下JDK是如何实现的,特别要注意JDK是如何保证同步和并发操作的。

什么叫可重入锁

可重入锁从字面意思就比较容易理解,自己获取了锁以后,可以再次获取该锁(重入)就叫可重入锁。

ReentrantLock获取锁原理

这里有个锁计数器的概念。重入锁实现机制就是基于一个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法。

这里区分公平和非公平锁。公平锁会在获取的时候判断队列中是否有比自己优先级高的线程若有则返回false。

当某一线程请求成功后,JVM会记下锁的线程持有者,同时将锁计数器置为1(cas实现);此时其它线程请求该锁,就只能等待,而该锁持有者却可以再次请求锁,同时锁计数器会递增,当线程退出同步代码块时,计数器会递减,直至锁计数器为0,则释放。

所以,如果ReentrantLock不手动释放锁,就会造成死锁。

ReentrantLock和synchronized对比

  • ReentrantLock等待可中断,synchronized不可以。
  • ReentrantLock需要手动释放锁,synchronized不需要。
  • ReentrantLock可支持公平非公平锁,synchronized只支持非公平锁。
  • ReentrantLock没有语言层面的优化,底层实现机制AQS和CAS,synchronized有优化。
  • ReentrantLock可重入锁,synchronized不可重入,可能导致死锁。
  • ReentrantLock支持读写锁,可以提高高并发读操作。
  • synchronized由操作系统支持,涉及内核态和用户态的上下文切换,并发高时切换开销非常大。
  • ReentrantLock(AQS)依赖volatile int变量标示锁状态,结构为双向链表的等待队列,通过(cas+死循环)更改锁状态,一旦更新成功,
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值