Java对象头:
若对象为数组,则虚拟机使用3个字宽存储对象头,否则,使用2个字存储对象头(64位虚拟机1字为8字节,64bit; 32虚拟机1字为4字节,32bit),具体如下图2-2所示:
其中Mark Word中存储着对象的HashCode、分代年龄、锁标记位。32位虚拟机的Mark Word如下图2-3所示(默认情况下):
运行期间,Mark Word中存储的数据可能会变化为下面四种:
注意:当要使用到对象的hashCode时,才会生成对象的hash码,这时因为要存储hashCode,对象的偏向状态将会丢失。
锁的升级与对比:
Java1.6中锁一共有四种状态:级别由低到高为:无锁,偏向锁,轻量级锁,重量级锁。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。
1、偏向锁:
引入原因:大多数情况下,锁总是由同一个线程多次获得。
实现细节:当一个线程尝试访问同步块获取锁时,会在对象头和栈帧中的锁记录中记录锁偏向的线程id,此后,该线程进入和退出同步块时只需测试一下对象头的Mark Word是否记录着指向当前线程的偏向锁,若是则成功获取锁。否则看Mark Word中偏向锁标识是否为1,若不是,则尝试CAS竞争锁,若是则尝试CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销:
1、出现锁竞争
当出现锁竞争时,拥有偏向锁的线程才会释放锁,释放偏向锁需要等待全局安全点(没有正在执行的字节码指令),首先需要暂停偏向的线程,然后检查偏向的线程是否活着,若活着,则遍历偏向对象的锁记录,栈中锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复无锁状态或进行锁升级。
2、调用锁对象的HashCode
偏向锁的hashcode存储在锁对象的MardWord中,当没有使用到对象的hashcode时,hashcode不生成,当使用了对象的hashcode,hashcode要占用markwork中25bit,就会覆盖锁对象偏向的线程id。(轻量级锁会在锁记录中记录hashCode,重量级锁会在Monitor中记录hashCode)
3、持有锁的线程调用wait/notify。
偏向锁的获得和撤销如下图所示:
偏向锁默认是延迟启动的,如果要关闭延迟::-XX:BiasedLockingStartupDelay=0。如果要关闭偏向锁:-XX:-UseBiasedLocking=false。关闭后,程序默认会进入轻量级锁状态。
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了T1的对象仍有机会重新偏向T2,重偏向会重置锁对象的ThreadID;
当撤销偏向锁阈值超过20次后,jvm会这样觉得,是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程;
批量撤销
当撤销偏向锁阈值超过40次,jvm会觉得,自己确实错了,根本不该偏向。于是整个类所有对象都会变为不可偏向的,新建的对象也是不可偏向的;
锁消除
内部对象加锁时,java即时编译器(JIT)会把synchronized优化掉(不存在竞争),可以使用 -XX:-EliminateLocks关闭。
2、轻量级锁:
加锁:
尝试获取锁之前,当前线程会先在栈帧中创建锁记录,并将对象头中Mark Word复制到锁记录中,然后cas将对象头的mark word 替换为指向锁记录的指针,若成功,则获得锁,否则尝试自旋获取锁。
解锁:
cas将锁记录中mark word替换回所对象中(之前所对象的mark word换为了指向锁记录的指针),若成功,则解锁成功,否则则存在竞争,锁会膨胀为重量级锁。流程如下:
总结:
(本文参考Java并发编程的艺术)