Java的悲观锁、乐观锁以及锁升级的一些概念

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

学习了一下JUC中相关的包,先从悲观锁和乐观锁开始,学习中涉及到一些偏向锁和 synchronized 锁升级的内容,但是并没有太深入,先记录一下学习的内容,之后如果有更深入的学习则会补充相关的知识点。


一、悲观锁

synchronized

最开始的synchronized本质是一把悲观锁,不论有没有人使用,synchronized都会把锁锁上。随着Java版本升级,synchronized引入了锁升级的概念,会逐步升级为悲观锁。
悲观锁的调度是依赖于操作系统的调度器的,阻塞或者唤醒一个线程是需要操作系统调度,此时会涉及到用户态和内核态之间切换。

二、乐观锁

乐观锁、自旋锁、无锁(无悲观锁)都是指的乐观锁。
CAS(Compare And Swap / SET /Exchange)实际上是乐观锁的一种实现方式,但是,目前乐观锁基本上只有这一种实现方式,所以CAS基本上等同于乐观锁

CAS操作

举例:定义一个变量i,然后一个线程将i加1并设置回去,当设置回去时,检查i的值是否还是之前线程拿过来的时候的那个值,如果是,那么将加1后的结果设置回去,否则,重新拿出新的值,再加1,然后再重复之前的操作,重新检查,然后如果值一致,则设置。
在Java中的JUC包,可以使用AtomicReference来实现CAS的操作。

ABA问题

在CAS将i值设置回去之前的检查步骤时,有可能会遇到ABA问题,即:该线程在获取的时候是0,然后将0加1变成1,准备设置回去,并检查,此时发现i还是等于0,这个时候按照计划应该是认为i没有被其他线程修改,并将i设置回去,但是实际上,i是有可能被其他线程修改为其他值(比如8或者9),然后又改回了0,这个时候,i在线程Th1设置时值为A,然后被其他线程(Th2)改为B,然后又被Thn修改回A,即遇到了ABA问题。

一般情况下,比如i为基本类型,或者其他一些情况下,实际上或许被其他线程修改回了A没有什么影响,但是如果这个i对象是一个引用类型,这个引用还在其他的地方使用,就可能会有一些问题产生了。

使用AtomicStampedReference/AtomicMarkableReference解决ABA问题

解决ABA问题,可以使用AtomicReference避免这个问题,他提供了两种解决方案,一种是AtomicStampedReference,它记录了一个版本号,当其中的对象被修改时,版本号会发生修改

AtomicStampedReference源码分析

  1. AtomicStampedReference中有一个叫Pair的内部类(内有两个字段,一个保存引用对象(reference),另一个保存标记号(stamp))。
  2. 在创建AtomicStampedReference时,需要同时传入两个参数,一个是reference,另一个就是stamp,他们会创建一个Pair对象,保存在AtomicStampedReference类的pair字段上。
  3. 调用getReference()方法时,会返回pair字段的reference值。
  4. 调用getStamp() 方法时,会返回pair字段的stamp值。
  5. 同时,还有一个get方法,该方法需要传入一个int类型的数组,方法内部会将pair字段的stamp值设置到该int数组到第1个元素上(下标为[0]的位置),然后该方法会返回pair的reference值。根据此方法,可以获取到当前AtomicStampedReference保存的引用对象和标记号。
  6. 另一个方法是 compareAndSet() 方法,这个值用来替换当前pair的值,从名字上看这个方法,就能理解为是CAS操作的主要方法了,这个方法接收了4个参数,分别是 预期的现pair保存的reference值、预期的现pair保存的stamp值、将要保存在pair的reference值、将要保存在pair的newStamp值,该方法会先判断预期的原reference的值是否与现有的值相同,然后会判断预期的原stamp是否与现有的值相同,之后再判断需要更新进pair的reference和stamp的值是否与现在pair存储的值已经相同,如果相同,则会返回直接返回成功,否则,会调用casPair()方法,去替换pair的值,然后返回替换结果(这里调用了sun.misc.Unsafe类的compareAndSwapObject()方法,这一块代码是native的,非Java实现,暂时没有看到JDK内部的代码,这次不再深入查看)。
  7. 从JDK1.8的代码来看,weakCompareAndSet()与compareAndSet()方法相同,调用weakCompareAndSet()方法时,实际上调用了compareAndSet()方法,内部没有任何其他的代码
  8. set() 方法: 调用set() 方法时,系统不会做CAS操作,当你传入的newReference和newStamp只要有一个与现在的pair字段中保存的不相同,则会创建新的pair对象,替换原来的pair字段的值。
  9. attemptStamp()方法:调用本方法,需要两个值,一个是预期当前pair中的reference值,另一个是新的newStamp,当预期当前pair中的reference值与pair中的值不同时,会返回失败,如果值相同,则会判断newStamp与pair中的stamp值是否相同,相同会直接返回true,否则会调用casPair() 方法更新值。

AtomicMarkableReference源码分析
AtomicMarkableReference中的源码基本上与AtomicStampedReference是相同的,但是只有一个区别,就是AtomicMarkableReference中的Pair类中的stamp是使用的boolean类型,而AtomicStampedReference是int类型,其他的方法功能都相同,但是将对应的int操作以及参数都变成了boolean类型。

CAS原子性问题

此问题在于,CAS操作本身应该是原子性的,就拿上边的那个介绍 CAS操作 的例子继续说,当线程Th1准备对变量i进行CAS操作的时候,需要首先判断变量i依然是0(初始值)才能将线程操作后的值设置回i中,但是现在有一个问题,就是在这个线程Th1判断完i值依然是0之后,准备将新的值设置进i时,另一个Th2线程进来,将i设置为了3,这个时候Th1点判断已经过去了,所以Th1会认为i没有被抢占,依然将自己线程计算之后的值设置进了i这个变量上,这种情况就是CAS的原子性问题。
在Java中,这部分的处理使用的是native方法,具体的源码在 unsafe.cpp源码 (记录以备之后学习)需要的可以看一下

三、偏向锁(JDK15以下)

偏向锁是在第一个线程使用锁时,将锁对象的ObjectHeder上设置上自己的线程号。偏向锁,是由于大多数场景下,其实都是单线程访问锁的情况偏多才产生的。当代码初次执行到synchronized时会变为偏向锁,此时会修改对象头中的标志位,将当前线程号写进去,等代码执行完成之后,并不会释放这把锁,等第二次的线程进来该对象时,会看ObjectHeader中的线程号是否是当前请求线程,如果是当前请求线程,则不用加锁就能重新进去,没有额外的开销。

四、乐观锁与悲观锁性能比较

使用悲观锁时,如果有一个线程正在使用对象,而其他线程过来之后,其他线程会排队等待锁释放之后再使用,这里是一个队列,锁释放之后操作系统会叫醒队列中的线程,目前大多使用的是CFS调度算法,此时其他的等待线程是不占用CPU资源的。乐观锁是依赖于程序本身的处理,会一遍一遍重复确认锁的状态,所以其实乐观锁是会占用CPU的资源。
所以,当任务时间比较长时,或者线程太多时,适合使用悲观锁,会减少CPU的性能消耗。但是如果是任务时间比较短,然后线程比较少的情况下,使用乐观锁会更合适。

五、优先使用synchronized

从Java1.5开始,synchronized加入了锁升级的逻辑。synchronized在使用的时候,会先从偏向锁开始,之后升级为自旋锁然后升级为悲观锁,这是会一步一步随着使用情况升级的。第一个线程进入的时候 ,会设置偏向锁,然后第二个线程进入的时候(有竞争的情况下)就会释放偏向锁(释放偏向锁会在safepoint(全局安全点))并变为自旋锁,然后随着使用情况按照需要(默认自旋锁自旋10次失败)升级为悲观锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值