synchronized的实现原理
Posted 迪巴哥没八哥
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了synchronized的实现原理相关的知识,希望对你有一定的参考价值。
synchronized的实现原理
大家好我是debug没有bug的迪吧哥,今天不加班,和大家来扯一扯synchronized。
Synchronized通常称为重量级锁,但是Java SE1.6后对synchronized进行了各种优化。Java中每个对象都可作为锁,具体有如下3种形式:
- 对于普通方法,锁是当前实例对象。
现象:先逐行输出mythread1,线程1:RUNNABLE,线程2:BLOCKED,之后暂停五分钟,逐行输出sleep end,mythread2
分析:线程2与线程1拥有相同的锁,线程1优先级高于线程2,线程1优先执行,获取到锁,执行sleep,未释放锁,线程2未获取到锁,处于阻塞状态,线程1sleep结束,输出sleep end,并释放锁,线程2获取到锁,执行输出mythread2
结论:当synchronized修饰普通方法时,锁对象即为当前对象
-
对于静态同步方法,锁的是当前类的class对象。
对上述代码稍作修改,在dosome方法上添加static关键字,main方法中,线程2对象初始化时,传入MyThread1.class
现象:与上述现象一致
分析:同上
结论:当synchronized修饰静态方法时,锁对象为当前Class对象
- 对于同步方法快,锁的是synchonized括号内的配置对象。
当一个线程试图访问同步代码块时,首先必须得到锁,退出或抛出异常必须释放锁。锁存在哪里? 锁有哪些信息?
JVM规范可看到Synchonized在JVM里的实现原理,JVM基于进入与退出Monitor对象实现方法同步与代码块同步。代码块同步使用monitorenter和monitorexit指令实现的,方法同步用另外方式实现,细节再JVM规范没详细说明。方法同步用这两个指令实现。
monitorenter指令在编译后插入到同步代码块开始位置,而monitorexit插入到方法结束与异常处,JVM保证每个monitorenter必须有相应monitorexit与之配对。任何对象有一个monitor与之关联,当一个monitor被持有后,它处于锁定状态。
我们执行以下代码,并且结合hsdis-amd64.dl工具查看控制台的字节码指令:
字节码指令:
我们可以看到 有ACC_SYNCHRONIZED, 它使用了monitorenter与monitorexit指令,隐式执行了Lock与Unlock操作,提供原子性。
synchronized锁的原理
jdk1.6后对Synchonized锁优化,有偏向锁、轻量级锁、重量级锁。
我们通过一下三个问题来了解synchronized原理:
- synchronized如何实现锁
- 为什么任何一个对象都可以成为锁
- 锁存在哪个地方?
了解synchronized前,我们需了解两个重要概念,一个是对象头,一个是monitor。
Java对象头:
在Hotspot虚拟机中,对象在内存的布局分为三块区域:对象头、实例数据与对象填充;Java对象头是实现synchronized锁对象基础,synchronized使用锁对象存储在Java对象头里。是轻量级锁与偏向锁关键。
Mawrk Word:
Mark word用于存储对象自身运行时数据,如哈希玛、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID\\偏向时间戳等。Java对象头一般占有两机器码,如下所示:
Monitor:
Monitor是什么? 可理解为一个同步工具,也是一种同步机制。Java对象是天生的Monitor,每个object对象里markOop->monitor()里可保存ObjectMonitor对象。
Synchronized如何实现锁?
了解了对象头和monitor之后我们可以分析synchronized锁的实现。
乐观锁:
乐观锁认为读多写少,每次拿数据的时候认为别人不修改,不会上锁,更新的时候判断这个期间又没人更新数据,写的时候先读出版本号,然后加锁操作,如果失败,重复操作。java中乐观锁基本通过CAS实现,比较当前值和传入值是否一样。
悲观锁:
悲观就是认为写多读少,拿数据时都认为别人会修改,所以每次读写会上锁,所以别人读写数据会block直到拿到锁。java中悲观锁是Synchronized,AQS框架的锁先尝试cas乐观锁获取锁,获取不到转化为悲观锁,如RetreenLock,还有数据库一般本身锁的机制也是基于悲观锁的机制实现的。
自旋锁(CAS):
自旋锁让不满足条件的线程等待一段时间,自旋就是一段循环,通过占用处理器时间避免线程切换带来的开销,但是如果持有锁定线程不能很快释放锁会浪费资源,下面是自旋锁代码演示。
偏向锁:
很多情况,锁没有存在多线程竞争,而是由同一线程多次获得,为了让线程获取锁代价变低引入偏向锁。下图是偏向锁获得与撤销图。
1、线程访问同步块获取锁,对想头与栈帧锁记录存储锁偏向的线程ID。
2、线程进入退出同步快不要cas操作,只需简单测试Mark Word是否存储线程偏向锁。
3、测试成功,表示线程获得锁,测试失败,测试Mark Word偏向锁标识是否为01,没有设置则使用CAS竞争锁,设置了。尝试用CAS把对象头偏向锁指向当前线程。
4、执行同步块,线程2访问同步块,检查对象头Mark Word是否存储线程2的偏向锁,不是侧进入cas替换,此时替换失败,线程1已经替换。
轻量级锁:
引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,下面是轻量级锁的流程图:
重量级锁:
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。这就是说为什么重量级线程开销很大的。
好了,今天的分享就到这里了,我是debug没bug的迪吧哥,你的点赞关注是对我的最大支持,感谢你的分享。
公众号、b站、知乎、csdn:迪巴哥沒八哥
以上是关于synchronized的实现原理的主要内容,如果未能解决你的问题,请参考以下文章