浅谈synchronized
没有系统学习过,导致有些概念其实都是从各种博客,教程上东拼西凑上拼接成的。没办法,以后学习只能靠自己了。
我这里总结的知识点,也只是b站黑马的juc课程。
但是真的讲的不错啊。
先讲现象
我们平常用来处理多线程的共享资源的问题的时候,都是用的synchronize。知道锁实例对象和锁类对象的区别。可是为什么呢。有人说是jvm底层的monitorenter指令。可是为什么这个指令就能做到锁的效果呢。我们以前是知道这个知识,但是不知道为什么。就想我们知道物质是原子构成的,但是物质性质为什么是这样的呢。这里又涉及到原子的电子了。
底层
每个对象有三个数据,mark word(对象头,这样每个对象都可以有专属的状态)。 Klass(指向类类型),属性数据。
syncchronized本来是利用对象头关联moniitor,monitor是jvm维护的数据,里面的结构是waitset,entiylist,owner。
这样通过锁monitor,就能关联对象和线程了。
线程1通过对象去获取锁对象,通过对象头找到moniitor。monitor发现owner没有线程占用,然后cas尝试改写owner,改写成功就获取了锁。
线程2这时候尝试通过对象去获取锁对象,发现owner已经有对象了,然后就进了entiylist阻塞了。
然后线程1从临界区运行完后,cas把owner置为空。然后通知entityList。这样entitylist里thread数组就可以被欢喜争取cpu了。
而保证锁的释放,是通过try,catch保证。
而waitset其实就是我们的object的wait和notify的实现结构了。
这样本来也能执行。但是这样的monitor太重量级了,因为每次新来到临界区,都要去申请一下monitor。维护要成本的啊。
而不是所有时候,都有这么多线程去抢的,然后往往是一个线程自己担心,其实没人抢你。也有就是竞争不是那么强烈。我会和你一起用,但是我们时间不冲突的啊。
所以,jvm优化了synchronize的实现,引入了轻量锁,什么是轻量锁呢,
也是对象头里,指向了一个Lock record地址(也是jvm维护),里面的结构是 hash code (因为你的lock record地址占用了原来的对象头mark word里的hash code数据)和对象地址。
线程1通过对象1获取锁对象,通过对象头找到了lock record。 然后发现暂时没人用过这个对象,那就申请了一个log record。维护了对象和线程的关系,lock record是线程中有的。
线程1又通过对象获取锁,发现已经有lock record了,而且还是自己和这个对象的,那就在申请一个log record。后续通过有多少个log record判断重入了多少锁。
如果这时候没有人来抢锁的话,就是线程1依次以栈的方式,释放lock record。这样最后,对象头又还原成没锁的样子。
如果有线程抢夺。
如果线程2来获取锁,发现哦吼,这个已经申请过lock record的了,然后不是我申请的。怎么办呢。那我自己申请一个monitor,自己进entityLIst等着。owner指向thread1.
然后等线程1执行玩最后,发现要还原对象头的时候,发现对象头变成执行monitor了,那我就把monitor的owner置为空呗,然后通知entitylist呗。
这样就从轻量锁膨胀为重量锁了。
这其实也是有点重了,有没有更简单的呢。 因为大部分,其实就是一个线程自己瞎担心。
这时候,jvm就说,你自己把线程ID(操作系统的线程,不是我们理解的线程id)写在对象头把。
这样,
线程1通过对象1看了下,没人动过这个对象头,那我就cas设置写上我的threadID.
下次来再申请的时候发现是偏向锁,然后一看对象头里的线程ID,发现原来是我用过的,那就不用管了,再进去呗。
这里的话,需要想想,偏向锁怎么升级为轻量锁呢。他设置了线程ID可没擦除啊。其实是这样的,
如果线程2来向对象申请锁,先看是否是可偏向的,如果是不可偏向的,那就说明这个已经是升级为轻量锁了。那就走轻量锁的逻辑。
如果,是可偏向的,然后看线程ID,如果是自己的,那说明是自己以前用过的。
如果不是我的线程ID,这个时候,jvm会stw,撤销偏向锁,撤销之前必须要检查线程A是不是还活着,因为此时线程A可能正在临界区内执行同步代码,也可能线程A活的很滋润。
看线程ID,如果线程ID已经死了,那说明可以用,那我cas写我的线程ID.
然后如果线程ID没死,锁升级为轻量级锁,在暂停的线程栈中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到 锁记录中,使用CAS将对象头中的Mark Word替换为指向锁记录的指针,恢复之前暂停的线程
JVM线程伪造一个displaced mark word到持有锁的线程的栈上,object的mark work更新为轻量锁模式。然后唤醒持有锁的线程。这样持有锁的线程就会自以为自己持有的是轻量锁了。而竞争者也会按照轻量锁的模式去竞争锁。
细节补充:
如何判断偏向所有者没有正在持有该偏向锁?
分两步,首先判断偏向所有者是否还活着,如果还活着,则遍历它的栈,看是否能找到关联该锁的锁记录,如果找到,则正在持有,如果没找到,则没有持有。(遍历过程在一个安全点执行,此时偏向所有者被阻塞。)
偏向所有者正在持有该偏向锁,如何将其撤销为轻量级锁?
遍历偏向所有者的栈,修改与该锁关联的所有锁记录,让偏向所有者以为它对该对象加的就是轻量级锁。
https://blog.csdn.net/weixin_42213903/article/details/97044043
https://blog.csdn.net/weixin_39568133/article/details/114757317
https://www.zhihu.com/question/57774162
https://www.jianshu.com/p/7445361e187f
这里又有个优化,因为偏向锁被升级为轻量锁,如果只是几个对象,那没事,而如果一定量级的话,那是不是说明,大人,时代变了。这个对象应该被这个对象使用呢。jvm在升级了几个这个类的对象后,会直接把其他的这个类的对象偏向新的对象(对象头的线程ID改为新对象)。
而如果升级了更大量级的话,是不是说明这个类对象竞争很激烈? 这个时候这个类对象直接是从轻量锁申请的。
jvm还会从编译的时候,优化一些没有的申请锁。比如在一个方法里,循环申请锁的话,那为什么不直接在方法上呢。这个就是锁消除。