一、需要了解的结构:
1.java普通对象头结构(数组不同,不赘述)
Mark word: 25位hashcode+4位分代年龄+3位锁标记
2.线程栈帧结构
Object reference: 用于指向对象地址
LockRecord: 锁记录对象,jvm层面,记录锁记录的地址和加锁状态
3.对象结构
二、重量级锁(Monitor)
1.正常情况下对象的Mark Word部分记录的是hashcode等信息;当对象被上锁以后,对象的Mark Word记录Monitor地址
2.当线程2需要调用obj时,会检查obj对应的Monitor的Owner是否已经被其他线程占用,如果没有,线程2占用obj
3.当其他的线程想要调用obj时候先看一下Monitor的Owner有没有被其他的线程占用,如果被占用,就挂到阻塞队列等待;
4.线程2使用完成后唤醒等待队列的线程,这些线程对obj进行竞争(非公平竞争,竞争方法由jdk实现)
问题: 重量级锁中的阻塞需要转入内核态中完成,花费时间多。很多时候不会有一个线程长时间占用同一个对象资源
三、轻量级锁
使用情景:一个对象有多个进程访问,但是线程访问的时间是错开的,用轻量级锁优化
1.Thread-0要使用obj,先在自己的栈中创建一个锁记录对象(Lock Record 各部分结构上面有提到)
2.检查obj的Markword字段,如果没有被上锁(结尾是01)就将LockRecord与MarkWord交换(这个比较交换过程叫cas,失败的情况之后说)
3.当Thread-0使用完成后在将Markword和LockRecord交换回来
cas失败情况:
[1] Thread-0自己再次申请obj(此时obj被Thread-0上锁,还没有被使用完)----锁重入
<1>再向Thread-0的栈中添加一个Lock Record对,并且将LockRecord字段为null
<2>以后每解锁一个为null的就去掉一个Lock Record栈帧,直到最后一个LockRecord为null的栈帧被去掉,开始进行还原过程
<3>启用cas过程,若成功正常解锁obj,如果不成功证明进行了锁膨胀或者已经升级为重量级锁,进入重量级锁的流程
[2] Thread-1也想要给obj上锁,但是obj已经别Thread-0占用(锁膨胀)
<1>Thread-1尝试与obj进行cas,obj被占用,cas失败
<2>为obj申请一个Monitor,obj的Markword指向Monitor地址
<3>Monitor的Owner指向Thread-0,Thread-1被阻塞,进入Monitor的阻塞队列
<4>当Thread-0使用完成,将Owner置为null,唤醒Monitor阻塞队列进行对obj的竞争
问题: <1>Thread-0占用时间不长,但是Thread-1与Thread-0冲突时马上转化为重量级锁耗费时间;<2>同一个线程一直重复给对象加锁,其他线程很少给对象加锁时会出现大量的cas操作,浪费时间
四、自旋优化
当Thread-1想要为obj上锁,但是obj已经被Thread-0占用时,Thread-1不马上进入阻塞态,而是自旋等几次,如果Thread-0释放obj就让Thread-1使用,否则进入阻塞队列(只使用多核cpu)
Java6会根据前几次的自旋结果设置自旋次数,如果前面有成功地例子那就多自旋几次,jdk7以后就不能控制自旋次数了
五、偏向锁
<1>Thread-0获得锁后,将obj的markword换成Thread-0的id
<2>以后每次都比较id是否相同,相同证明Thread-0已经获得锁,不用锁重入
<3>如果有新的Thread-1想获得锁,进入锁膨胀,偏向锁变为轻量级锁
Tips:
[1]轻量级锁要进行每次都要cas更新markword,偏向锁默认markword就是一个线程的id不会自己改变所以偏向锁无法自旋优化
[2]调用wait/notify时会将偏向锁和轻量级锁都升级为重量级锁
[3]当一个可以偏向的对象被调用Hashcode时会撤销该对象的偏向状态(因为hashcode占用mark word的)
31位,而线程id用54位,Markword一共64,不够用)
批量重偏向
当偏向锁撤销超过20次的时候,会执行批量重偏向
批量撤销
当偏向锁撤销超过40次后,证明此对象的竞争比较激烈,所以将它置为不可以偏向状态
锁削除
场景: 实际过程中,很有可能很多加锁是无效的(如局部变量作为锁,由于每次都是新对象新锁,所以没有意义)
虚拟机即时编译器(JIT)运行时,依据逃逸分析的数据检测到不可能存在竞争的锁,就自动将该锁消除