Java中synchronized的底层实现原理

发表于:2022-12-29 09:15

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:枫林晚粥    来源:今日头条

#
Java
分享:
  一、对象头、Mark Word、monitor、synchronized怎么关联起来
  (1)首先java里面每个对象JVM底层都会为它创建一个监视器monitor,这个是JVM层次为我们保证的。这个监视器就类似一个锁,哪个线程持有这个monitor的操作权,就相当于获取到了锁
  (2)其次synchronized 修饰的代码或者方法,底层会生成两条指令分别为monitorenter、monitorexit。
  (3)进入synchronized的代码块之前会执行monitorenter指令,去申请monitor监视器的操作权,如果申请成功了,就相当于获取到了锁。如果已经有别的线程申请成功monitor了,这个时候它就得等着,等别的线程执行完synchronized里面的代码之后就会执行monitorexit指令释放monitor监视器,这样其它在等待的线程就可以再次申请获取monitor监视器了。
  monitor又是个啥东西?为什么monitor能当做锁?首先既然你知道每个对象都有一个monitor监视器,那你知道每个对象是怎么和它的monitor监视器关联起来的不?
  通过synchronized进行加锁,就是通过对象头的Mark Word关联起来的,里面记录着锁状态和占有锁的线程地址指针。
  当Mark Word中最后两位的锁标志位是10的时候,Mark Word的前面是monitor监视器的地址,我现在就给你画出来对象头、Mark Word 和 monitor之间的关系图(32位):
  二、monitor内部结构
  monitor叫做对象监视器、也叫作监视器锁,JVM规定了每一个java对象都有一个monitor对象与之对应,这monitor是JVM帮我们创建的,在底层使用C++实现的。
  其实monitor在C++底层也是某个类的对象,那个类就是ObjectMonitor,它拥有的属性也字段如下:
  //结构体如下
  ObjectMonitor::ObjectMonitor() {
  _header;
  _count ;  // 非常重要,表示锁计数器,_count = 0表示还没人加锁,_count > 0 表示加锁的次数
  _waiters;
  _recursions;
  _owner; // 非常重要,指向加锁成功的线程,_owner = null 时候表示没人加锁
  _waitset;  // wait线程的集合,在synchorized代码块中调用wait()方法的线程会被加入到此集合中沉睡,等待别人叫醒它
  _waitsetLock;
  _responsiable;
  _succ;
  _cxq;
  _freenext;
  _entrylist;  // 非常重要,等待队列,加锁失败的线程会被加入到这个等待队列中,等待再次争抢锁
  _spinFreq;  // 获取锁之前的自旋的次数
  _spinclock; // 获取之前每次锁自旋的时间
  ownerIsThread;
  }
  3.1、monitor加锁原理
  _count : 这个属性非常重要,直接表示有没有被加锁,如果没被线程加锁则 _count=0,如果_count大于0则说明被加锁了。
  _owner:这个属性也非常重要,直接指向加锁的线程,比如线程A获取锁成功了,则_owner = 线程A;当_owner = null的时候表示没线程加锁。
  _waitset:当持有锁的线程调用wait()方法的时候,那个线程就会释放锁,然后线程被加入到monitor的waitset集合中等待,然后线程就会被挂起。只有有别的线程调用notify将它唤醒。_entrylist:这个就是等待队列,当线程加锁失败的时候被block住,然后线程会被加入到这个entrylist队列中,等待获取锁。
  _spinFreq:获取锁失败前自旋的次数;JDK1.6之后对synchronized进行优化;原先JDK1.6以前,只要线程获取锁失败,线程立马被挂起,线程醒来的时候再去竞争锁,这样会导致频繁的上下文切换,性能太差了。JDK1.6后优化了这个问题,就是线程获取锁失败之后,不会被立马挂起,而是每个一段时间都会重试去争抢一次,这个_spinFreq就是最大的重试次数,也就是自旋的次数,如果超过了这个次数抢不到,那线程只能沉睡了。_spinClock:上面说获取锁失败每隔一段时间都会重试一次,这个属性就是自旋间隔的时间周期,比如50ms,那么就是每隔50ms就尝试一次获取锁。
  下面通过图文展示加锁过程:
  (1)首先呢,没有线程对monitor进行加锁的时候是这样的:
  说明:_count = 0 表示加锁次数是0,也就是没线程加锁;_owner 指向null,也就是没线程加锁。
  (2)然后呢,这个时候线程A、线程B来竞争加锁了,如下图所示:
  (3)线程A竞争到锁,将_count 修改为1,表示加锁次数为1,将_owner = 线程A,也就是指向自己,表示线程A获取到了锁。在_count = 0,_owner = null的时候,表示monitor没人加锁,这个时候线程A和线程B同时请求加锁,也就是竞争将_count改为1。由于线程A这哥们动作比较快,它将_count改为1,获取锁成功了。它还嘚瑟了一下,同时将_onwer = 线程A,表示自己获取了锁,告诉线程B,兄弟不好意思了,是我获取了锁,我先去操作了。
  既然加锁就是将_count 设置为1,同时将_owner 指向自己。那反过来推测,释放锁的时候是不是将_count 设置为 0 , 将 _owner 设置为 null 就 OK了?是的,释放锁的过程就是这么简单:
  加锁和释放锁说完了,我们接下来将的是
  _spinFreq、_spinclock、_entrylist
  这几个东西:
  上面解释字段属性的时候说_spinFreq是等待锁期间自旋的次数、_spinclock是自旋的周期也就是每次自旋多久时间、_entrylist这个就是自旋次数用完了还没获取锁,只能放到_entrylist等待队列挂起了。
  让我们继续接着图来讲:
  (1)首先线程B获取锁的时候发现monitor已经被线程A加锁了(2)然后monitor里面记录的_spinFreq 、spinclock 信息告诉线程B,你可以每隔50ms来尝试加锁一次,总共可以尝试10次(3)如果线程B在10次尝试加锁期间,获取锁成功了,那线程B将_count 设置为 1,_owner 指向自己表示自己获取锁成功了(4)如果10次尝试获取锁此时都用完了,那没辙了,它只能放到等待队列里面先睡觉去了,也就是线程B被挂起了
  _spinFreq和_spinclock 这两个monitor的属性主要是让线程自旋的时候使用的吧。
  entryList作用是当线程自旋次数都用完了之后,只能进入等待队列进行休眠了。
  4.6、轻量级锁
  轻量级锁模式下,加锁之前会创建一个锁记录,然后将Mark Word中的数据备份到锁记录中(Mark Word存储hashcode、GC年龄等很重要数据,不能丢失了),以便后续恢复Mark Word使用。这个锁记录放在加锁线程的虚拟机栈中,加锁的过程就是将Mark Word 前面的30位指向锁记录地址。所以mark word的这个地址指向哪个线程的虚拟机栈中,就说明哪个线程获取了轻量级锁。就好比下面的图,线程A获取了轻量级锁,锁记录存在线程A的虚拟机栈中,然后Mark Word的前面30位存储锁记录的地址。
  了解了轻量级加锁的原理之后,我们继续,来讲讲偏向锁升级为轻量级锁的过程:
  (1)首先线程A持有偏向锁,然后正在执行synchronized块中的代码。
  (2)这个时候线程B来竞争锁,发现有人加了偏向锁并且正在执行synchronized块中的代码,为了避免上述说的线程A一直持有锁不释放的情况,需要对锁进行升级,升级为轻量级锁。
  (3)先将线程A暂停,为线程A创建一个锁记录Lock Record,将Mark Word的数据复制到锁记录中;然后将锁记录放入线程A的虚拟机栈中。
  (4)然后将Mark Word中的前30位指向线程A中锁记录的地址,将线程A唤醒,线程A就知道自己持有了轻量级锁。
  4.6.2、在轻量级锁模式下,多线程是怎么竞争锁和释放锁的?
  (1)线程A和线程B同时竞争锁,在轻量级锁模式下,都会创建Lock Record锁记录放入自己的栈帧中。
  (2)同时执行CAS操作,将Mark Word前30位设置为自己锁记录的地址,谁设置成功了,锁就获取到锁。
  上面讲了加锁的过程,轻量级锁的释放很简单,就将自己的Lock Record中的Mark Word备份的数据恢复回去即可,恢复的时候执行的是CAS操作将Mark Word数据恢复成加锁前的样子。
  Java synchronized偏向锁后hashcode存在哪里?
  jdk8偏向锁是默认开启,但是是有延时的,可通过参数: -XX:BiasedLockingStartupDelay=0关闭延时。
  hashcode是懒加载,在调用hashCode方法后才会保存在对象头中。
  当对象头中没有hashcode时,对象头锁的状态是 可偏向( biasable,101,且无线程id)。
  如果在同步代码块之前调用hashCode方法,则对象头中会有hashcode,且锁状态是 不可偏向(0 01),这时候再执行同步代码块,锁直接是 轻量级锁(thin lock,00)。
  如果是在同步代码块中执行hashcode,则锁是从 偏向锁 直接膨胀为 重量级锁。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号