分享自己学习的Synchronized

学完并发已经忘得差不多了,看到Synchronized关键字,我在大脑搜索了一下对他的印象只剩一个“锁”字,juc的东西从学开始就不怎么理解,今天就想好好学习下Synchronized到底是什么。

对象头

想了解Synchronized就先得知道对象头这个东西,在java中,我们所有的对象都有对象头,对象头用来存储与对象自身定义无关的一些数据。

如图所示,对象头分为两部分(数组对象会多一部分来存储数组长度),Klass Word部分是用来存储该对象元数据的地址,就是这个对象是哪个类的实例。MarK Word用来存储对象hashcode,锁指针,垃圾回收分代年龄等,它的长度根据虚拟机的来的,32位的虚拟机它就是32bit。这个Mark Word就是了解锁这个概念的关键了,接下来看下它的结构。

Mark Word会根据对象的状态变更存储内容,正常状态下前25bit存储hashcode,然后4bit存储垃圾回收机制中的分代年龄,处于偏向锁状态的时候在后面1bit存储的值为1,其他状态为0,最后2bit用来存储锁的状态,不同的锁值也不一样。当不同锁状态下里面存储的分别是什么我们下文再说,先对这个对象头有个了解。

Monitor

现在回到Synchronized,我们之前总是会听说Synchronized是重量级锁,这个根本原因就在于这个Monitor,有叫它管程机制的,也有叫他对象监视器机制的,无所谓,下面就叫它Monitor好了。

下面我们根据这个图来详细解释一下Monitor和Synchronized。

当我们使用Synchronized锁住一个对象obj时(当Synchronized加在方法上锁的也是该类的class对象),操作系统会新建一个Monitor对象,用obj的对象头(之前讲的Mark Word中的前30bit)来存储指向这个新建的monitor对象的指针或者说地址。这个Monitor对象是操作系统层面的对象,Monitor对象又分为三部分,Owner就是用来记录当前这个锁的持有者线程,图中的Owner区域存储了Thread-2的线程id,说明当前是Thread-2持有了这把锁。这个时候,如果Thread-3来尝试执行被锁住的临界区的代码时,就会根据obj的对象头中记录的地址找到这个Monitor对象,然后访问它的Owner,发现如果有值了(默认为null)就说明这把锁有持有者,那么Thread-3就会被记录在Monitor的EntryList里面。EntryList是一个阻塞队列,Monitor对象会把尝试获取锁没成功的都会被记录到EntryList中,当Owner空了也就是持有者执行完毕后,就会去唤醒EntryList中的线程,(唤醒过程也是公平竞争的,先进来等待的不一定先被唤醒)monitor会把新获得锁的线程id记录在Owner里。【waitset是juc中wait()和notify()方法用到的等待区,这里不做详解】

我们每用Synchronized锁住的一个java对象时,都会对应新建一个操作系统的Monitor对象,这也是为什么synchronized保证线程安全必须是锁住同一个对象的原因。

上面我们了解了Monitor,因为Monitor是操作系统层面的对象,与操作系统交互消耗是比较大的,这也是为什么说Synchronized是重量级锁,使用Monitor锁住对象的时,对象头就是处于之前提到的下图这个状态,前面30bit变成存在Monitor对象的地址,最后2bit的值为10,标志此时该对象处于重量级锁状态。

那除了这个状态,我们刚刚还看到很多的别的状态,没错,Synchronized没有那么简单,往下看。

锁机制

Synchronized并不是所有的上锁都上Monitor,因为Monitor这种操作系统来支撑的太过重量级。所以Synchronized提供了一套锁膨胀的机制。

轻量级锁

首先我们需要明白为什么会有轻量级锁,先看一种情况:虽然存在多个线程共享同一个资源,但是这多个线程并不会产生竞争,也就是一个用完了,过一段时间另一个接着用,这种情况下如果继续使用Monitor这种重量级锁会产生无意义的消耗,等于我上锁了但是在我用的过程中根本没人来强,那我何必每次都做“上锁”这么一个无意义的操作呢。

考虑上面这个情况才有了轻量级锁,轻量级锁的上锁并没有用到Monitor对象,给对象上轻量级锁时,java虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,然后将对象头中的的前30bit存储的hashcode等信息复制一份存到Lock Record中,当做一个备份,那么现在对象头中这个30bit就可去去存别的东西,存什么呢,就是指向这个Lock Record的指针,然后再将翠香头最后标志锁信息的2bit改为00,说明此时是轻量级锁状态。没错,其实就是做了一个交换,那么之后所有线程都是通过cas的方式来尝试将对该对象进行这种交换,如果发生竞争就会膨胀升级为重量级锁,Synchronized对轻量级锁还有升级,支持可重入,考虑到篇幅太长不易理解,具体过程可以自己了解。

偏向锁

有了轻量级锁之后,还是觉得会有无谓消耗,我们们看这种情况:还是有多个线程共享同一个资源,但是每次其实是由同一个线程多次获取,其他线程很少或者几乎不获取,那么那个频繁获取的线程每次都要去cas获取轻量级锁的操作是无意义的,如果总是我使用,我为什么都要去做一个上锁解锁的操作呢,就是这样一个想法,就出现了偏向锁。

偏向锁顾名思义,就是说我认为这个资源是偏向我的,或者说我就认为这是我一个人用的。偏向锁的机制就是将对象头中mark word里的前23bit替换为线程id(这里的线程id不是java中thread.getId得到的id,而是操作系统为该线程分配的id),后面epoch用2bit存储偏向时间戳,然后将最后三位改为101(正常状态为001),再次访问时,如果id仍是自己则直接使用,这种操作类似于把自己名字刻上去了,就算我不用了,我也认为别人不能用我的。

偏向锁有一个重偏向机制,就是判断到底是该升级为轻量级锁还是继续用偏向锁的机制。当一个线程的偏向锁转为轻量级锁的次数在19到39之间时,偏向锁支持批量重偏向(将线程id记为另一个线程),1到19直接升级为轻量级锁,超过39触发批量撤销机制,jvm认为该类不适合使用偏向锁,就会撤销偏向锁,都直接升级为轻量级锁,这个比较抽象,有兴趣也可以自己去了解。

还要提的一点是,对象创建之后默认是开启偏向锁的(mark word最后三位为101),但是由于偏向锁存在延迟,所以在程序启动之初创建的对象会是正常状态(mark word最后三位为001),在程序启动几秒钟之后创建的对象就是默认开启偏向锁了,也可以通过虚拟机参数指定延迟为0。

调用对象的hashcode方法(偏向锁线程id占用了hashcode的存储位置)和使用vm参数可以禁用偏向锁。

锁膨胀

看完前面也大概明白所膨胀是什么了,所膨胀就是Synchronized刚开始并不会给对象上Moniter锁,而是偏向锁>>轻量级锁>>Monitor(重量级锁)这样一个顺序,这个过程就称之为所膨胀。这是一个单向的过程,也就是重量级锁无法回归轻量级锁。

锁消除

jvm的即时编译器(jit)在编译代码的时候会对锁进行优化,把它认为无意义的锁去除。

总结

总的来说,Synchronized是一个重量级锁是因为它的Monit机制,但其实它的锁机制赋予了它更高的可用性,以后一般情况下可以放心大胆地用,不要再信别人太重量级、消耗大高之类的话了。

【菜鸟博文,如有不正请指出,网图侵删】

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值