32位操作系统:
64位操作系统:
obj对象的MarkWord中的指针(ptr_to_heavyweight_monitor)指向一个 OS提供的 Monitor对象
Monitor中的Owner记录谁是这个锁的主人。
当另一个对象也要获取obj锁时:发现obj所指向的Monitor的所有者为Thread1,此时Thread2加入 阻塞队列
当Thread1执行完毕,释放锁后,虚拟机从obj对象指指向的Monitor的EntryList中唤醒一个线程,赋给它锁。使用场景:如果一个对象虽然有多个线程访问,但是多线程访问的时间是错开的(没有竞争),那么可以使用 轻量级锁 来优化。
轻量级锁的语法仍然是synchronized
static final Object obj = new Object(); public static void method1(){ synchronized(obj){ //1 加锁 method2(); }//4 } public static void method2(){ synchronized(obj){ //2 ... }//3 }加锁:
在栈帧中创建锁记录(Lock Record)对象(代码1):左边是栈帧,右边是Java堆中的obj对象,每个线程的栈帧都会包含一个锁记录的结构,其中存储了锁定对象的Mark Word。
让锁记录中Object reference指向锁对象,并尝试使用cas替换Object的Mark Word,将Mark Word的值存入锁记录。 如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁 如果cas失败,有两种情况:如果有其它线程持有了obj的轻量级锁,这是表明有竞争,进入锁膨胀过程
如果是自己执行了synchronized锁重入 (代码中2的位置),那么再添加一条LockRecord作为重入的计数
这里也会进行cas替换操作,但是会失败。
解锁:
在代码3的位置,退出synchronized代码块,如果有取值为null的所记录,表示有重入,这是重置锁记录,表示重入计数减一 在代码4的位置,退出synchronized代码块,此时所记录的值不为null,这是使用cas将Mark Word的值恢复给obj对象头成功,解锁成功
失败,说明轻量级锁进行了膨胀或已经升级为重量级锁,进入重量级锁解锁流程
Thread-0执行method1方法,获得到轻量级锁,此时Thread-1执行method1方法,获取轻量级锁失败,进入锁膨胀流程:
为ojb对象申请 Monitor 锁,让obj指向重量级锁地址,并且对象头所标志位改为10
然后自己进入Monitor的EntryList 进入阻塞队列
当Thread-0 退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入 重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程当轻量级锁竞争时,会先进行自旋等待锁,如果自旋没有获得锁,才会膨胀为重量级锁
轻量级锁在每次锁重入时,仍然需要执行CAS操作,JAVA 6 中引入了偏向锁,来优化锁轻量级锁的锁重入。
static final Object obj = new Object();//1 public static void method1(){ synchronized(obj){ //2 method2(); }//5 } public static vpid method2(){ synchronized(obj){ //3 //临界区 }//4 }1: 此时obj的对象头的Mark Word为:
2:假设Thread-1是第一个获取该锁的线程,其线程ID为100,那么此时的Mark Word为:
3:这里是一个锁重入,此时obj的对象头的Mark Word与2一样
4:在重入锁释放锁的时候,偏向锁使用了一种 等到竞争出现才释放锁 的机制,这里也不会改变Mark Word ,同理在代码5处释放锁的时候也不会改变对象头
public static void test1(){ Object lock = new Object(); Thread t1 = new Thread(()->{ log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); synchronized (lock){ log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); } log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); },"t1"); t1.start(); Thread t2 = new Thread(()->{ try { t1.join();//等待t1线程终止 } catch (InterruptedException e) { e.printStackTrace(); } log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); synchronized (lock){ log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); } log.info(getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); },"t2"); t2.start(); }运行结果:
从上面的运行结果可以看到:t2线程在获取锁的时候,因为对象头的线程id不是t2线程的id,所以偏向锁 升级为了轻量级锁;并且在释放锁之后,lock对象的Mark Word 被设置为无锁状态(001),那么下次线程获取改锁时会是一个 轻量级锁。
对象的哈希码是存放在Mark Word 中的,但是一旦存放了哈希码,Mark Word就没有多余的空间来存放线程Id了,此时lock对象的Mark Word 的锁状态就变为了无锁(001)。
其他线程竞争锁如前面demo的结果所示,t2线程在t1线程终止之后获得了一个轻量级锁。t2在申请锁的过程中,偏向锁被撤销,然后升级为轻量级锁赋给t2线程,t2线程释放锁后,lock对象的锁状态为无锁状态。
在上面的偏向锁撤销中,我们发现当一个线程去竞争偏向其他线程的偏向锁时,偏向锁会撤销。偏向锁的撤销有这样一个机制:
如果有线程t竞争偏向其他的线程的次数累计达到19次,那么第19次之后竞争的偏向锁将会偏向线程t(线程t获得的不再是轻量级锁),这个机制叫做 批量重偏向。
public static void test2() throws InterruptedException { List<Object> locks = new ArrayList<>(); Thread t1 = new Thread(()->{ for (int i = 0; i < 30 ; i++){ Object lock = new Object(); locks.add(lock); log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); synchronized (lock){ log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); } log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); } },"t1"); t1.start(); Thread t2 = new Thread(()->{ try { t1.join();//等待t1结束 } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i < 30; i++){ Object lock = locks.get(i); log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); synchronized (lock){ log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); } log.info( i + " : {}",getMarkWord(ClassLayout.parseInstance(lock).toPrintable())); } },"t2"); t2.start(); }运行结果:
可以看到第19个锁还是轻量级锁,第20个锁已经偏向了t2。
t2线程撤销了0-18个锁,19-38个锁进行了重偏向线程t2。
t3线程撤销了19-38个锁,在JVM中,Lock类型的锁一共被撤销了39次,我们看新建一个Lock对象,它的所类型如下:
从结果可以看到,新创建的Lock对象不再试偏向锁了。这就是偏向锁的批量撤销机制:
在JVM中,属于同一类型的偏向锁被撤销大于等于39次,以后新建该类型对应的锁将不再是偏向锁。
如果上面的代码被反复的执行超过一个阈值,JIT即时编译器就会优化这部分的字节码。其中,类似obj这样的 局部变量,它除了在本方法中可以被访问到,其他任何地方都无法访问,所以这里的同步块就没有任何意义,JIT就会优化这个同步块,取消同步操作。
我们可以使用-XX:-EliminateLocks来关闭锁消除优化。