一、对象头、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,它拥有的属性也字段如下:
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,则锁是从 偏向锁 直接膨胀为 重量级锁。