并发学习(一)终于明白了synchronized的实现原理

前提

大家应该都了解synchronized是能够保证线程间的可见性以及原子性的吧,如果不了解,赶紧去看下我之前的文章。线程间的可见性

synchronized的几种用法

我们平常使用synchronized其实也是非常简单,在方法名和代码块前面加下就OK,先来回忆下。

1.修饰一般的方法,作用对象是当前对象的实例。
2.修饰代码块,指定加锁对象,作用对象就是加锁对象。
3.修饰静态方法,作用对象就是当前类。
由此可见,加锁的粒度要么是作用对象的实例,要么是作用整个类对象。
如果以上三点还没搞明白的童鞋,建议找相关资料再去搞懂一下,不然后面的原理更加难懂。

回顾下synchronized功能

一个线程要去访问synchronized修饰的方法或代码块,那么它就必须要获得一把锁。然后等方法执行结束后才会将锁释放,期间其他所有的线程只能在外面干等着。
通俗一点,某某大型互联网展览大会,参会人员需要VIP金卡才能进入。张三得到了一张金卡,但是李四王五没有。于是他们商量先让张三去里面参观,李四和王五在外面等着,张三参观完后再将卡给到李四和王五。
现在就需要思考下,这个锁(VIP金卡)是种身份权限的象征,那么在java里它是存放哪里的呢?
代码块synchronized(lock)是通过lock这个对象来控制锁粒度的,这里不妨大胆猜测lock对象跟锁一定是存在必然的联系。

需要了解下对象存储

lock既然是对象,那么它就一定会存放在堆内存中。而参考Hotspot虚拟机,对象在内存中的存放又可以分为三个部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
在这里插入图片描述
对象头里面的Mark Word是重点,它记录了对象和锁有关的信息。它里面存储的数据会随着锁标志位的变化而变化,因此可能存在以下5种情况。
在这里插入图片描述
既然是根据锁标志位的变化而变化,那么锁标志位是怎么变化的呢?
线程在获取锁的时候,其实是获得了一个监视器对象(monitor),而多个线程同时去访问synchronized修饰的代码时,相对于就是去抢对象监视器,然后修改对象中的锁标识。

锁的升级

为啥要搞多把锁?
现在就基于前面的5种情况来展开,看看每种情况都是如何转变的。前面可以看到有偏向锁、轻量级锁和重量级锁,我会首先思考干嘛弄这么多锁,一把锁搞不定吗?答案是,一把锁可以搞定,多把锁是为了提升性能。在jdk1.6之前,其实synchronized底层就只有重量级锁,也就是说,只要有线程竞争,那么就会存在一个个排队,并且出现各种获得锁以及释放锁的动作,这样就带来了很大的性能开销。
现在站在提升性能的角度思考下,假设没有锁,那么数据不安全;有了锁,性能大打折扣。那么,就要想方设法的来平衡这两者的关系了,既然提升性能,又要保证数据安全性。于是乎,jdk1.6之后就搞了多把锁,这也是对synchronized的一种优化。

从业务概率角度出发
举个例子,在并发场景下,我写了一个synchronized修饰的方法,来保障数据的安全性。在一些情况下会发现,不总是有多个线程竞争这个锁的,有时候就是同一个线程不断的获得锁和释放锁。然后从概率角度出发,比如一段时间里,会有多少概率出现这种情况。我如果把这种情况做下特殊优化,不就让性能提升了一点吗?于是乎,就有了偏向锁。然后,基于这种情况继续做优化,于是乎又有了轻量级锁。最后,实在是没法优化了,于是乎就只能是重量级锁了。
总结就是: 优化之前,不管啥情况,直接重量级锁,这样获得锁和释放锁会带来很大的性能开销。优化之后,对于很多情况可以直接偏向锁或轻量级锁就搞定了,实在搞不定才升级到重量级锁,这样就提升了性能(偏向锁和轻量级锁带来的性能开销很小)。

认识偏向锁

下面就开始来认识一下偏向锁,它是完全基于同一线程多次获得锁和释放锁而设计的。当线程访问加锁的同步代码块时,会在对象头中存储当前线程的ID,之后线程访问同步代码块时,直接去比较对象头里是否存储了指向当前线程的偏向锁(是否存储了当前线程的ID),如果比较成功,就不需要再去获得锁了。

偏向锁的获得过程
1.先获得锁对象的Mark Word,判断是否处于可偏向状态(偏向锁为1,线程ID为空)。
2.如果满足可偏向,则通过CAS(数据库乐观锁)操作,将当前线程ID写入到Mark Word。如果CAS写入成功,则表明该线程已经获得偏向锁,如果CAS写入失败,说明已经有其他线程获得了偏向锁,这种情况就说明存在多线程竞争了,那么就要撤销已获得偏向锁的线程。
3.如果是已偏向,那就需要检查Mark Word里的线程ID是否等于当前线程ID。如果相等,则不需要再去获得锁,直接执行代码。如果不相等,说明已经有其他线程获得了偏向锁,这种情况就说明存在多线程竞争了,那么就要撤销已获得偏向锁的线程。

偏向锁的撤销过程
在前面就已经介绍过,偏向锁局限于同一个线程反复获得锁和释放锁。既然存在多线程竞争情况了,说明偏向锁就已经不适用了。在撤销时,原来获得偏向锁的线程如果退出了临界区(最后的字节码执行完),这时对象头又会被设置成无锁状态,其他线程又可以根据CAS继续偏向了。如果原来线程还在临界区之类(还在跑代码),那就会将该线程升级为轻量级锁,然后继续执行。
在这里插入图片描述

认识轻量级锁

从偏向锁升级到轻量级锁,对象的Mark Word也会变成轻量级锁的存储方式。具体过程如下:
1.线程在自己的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word复制到锁记录中。
2.线程尝试使用CAS将对象头中的Mark Word指向锁记录的指针(其实就是比较锁记录里的Mark Word是否还等于对象头中的Mark Word)。
3.如果成功,当前线程获得锁。如果失败,说明其他线程获得锁。当前线程尝试通过自旋来获得锁。

什么是自旋锁?
顾名思义,通过自旋获得的锁就是自旋锁。那么什么又是自旋呢?所谓自旋,就是指当有另一个线程来竞争锁时,这个线程会在原地等待(注意这个等待不是阻塞,它是直接一个for死循环不断的去识别锁是否释放,这这个过程也会消耗CPU资源),直到获得锁的线程释放锁之后,这个线程就可以立马获得锁。
既然轻量级锁是存在一个原地等待的过程,如果等待时间很久呢?我们期望的是,原地等待时间都特别短,这样才能发挥出轻量级锁的特点,时间太长就不适用轻量级锁了。

怎么合理分配原地等待
轻量级锁的原地等待假设出现了最坏的情况,等待时间特别长,这时候肯定是要做特殊处理的,不可能无限制的循环。处理方式有以下几种:
1.指定默认循环次数,比如默认10次(jdk1.6之前)。
2.不采用默认,而是自适应自旋(jdk1.6之后)。它是根据前一次在同一个锁上自旋的时间以及锁的拥有者状态来决定。
3.比如,同一个锁对象上,这个等待的自旋锁刚刚才获得过锁,那么虚拟机就会认为你很快还能继续获得锁,所以允许你等待时间稍微长一点。
4.而对于某个锁,自旋很少成功获得过,那么以后再获取这个锁时,就懒的去自旋了,直接去阻塞线程。

轻量级锁解锁过程
轻量级锁的释放过程跟获得过程完全发过来,通过CAS将线程栈帧中的锁记录替换回对象头Mark Word中,如果成功,表示没有锁竞争。如果失败,表示当前锁存在竞争。那么轻量级锁就会发生锁膨胀,升级为重量级锁。

认识重量级锁

到了重量级锁等价于又回到了jdk1.6之前的情况了,确实是没得优化了。多个线程访问同步代码块,只能将未获得锁的线程给阻塞。

总结

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值