以下都是基于jdk8的版本,并且需要了解对象中Mark Word、Klass Ponint等结构,这里只做验证,内存依赖用的是0.9版本的,二进制是倒着输出的,所以图中红色框和绿色框仅仅代表位置,不是很准确,大家知道这一点就好了
- java是默认开启偏向锁,但是是延迟开启,即在系统启动的一段时间内是没有偏向锁的,几秒后,偏向锁开启。以下命令可以调整相关参数:
//关闭延迟开启偏向锁 -XX:BiasedLockingStartupDelay=0 //禁止偏向锁 -XX:-UseBiasedLocking //启用偏向锁 -XX:+UseBiasedLocking
- 验证默认情况下锁机制,先引入对象内存分析依赖包
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
- 测试代码,ClassLayout.parseInstance(new Object()).toPrintable():打印对象内存相关信息
public static void main (String[] args) throws Exception { //这里需要打印不同的对象,因为打印同一个对象的话,已经加载了,状态前后就一致了,观察不到效果 System.out.println("启动时:" + ClassLayout.parseInstance(new Demo()).toPrintable()); Thread.sleep(4000); System.out.println("启动后:" + ClassLayout.parseInstance(new Demo()).toPrintable()); }
以上只验证默认情况,其他情况可以在idea中增加vm的启动参数进行验证
- 现在关闭偏向锁延迟,验证锁升级的几种情况,
(一定要关闭延迟,否则验证结果会不同)
-
先申明一点,开启了偏向锁,但是没有synchronized代码块时,对象的Mark Word中会存储
线程id、偏向锁的标志位,锁的标志位
信息,没有竞争的情况下,线程id全是0,代表没有线程竞争过,当有线程开始竞争时,线程id将改为当前线程。测试代码如下:public static void main (String[] args) throws Exception { Demo obj = new Demo(); //加锁的对象 System.out.println("加锁前:" + ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj) { System.out.println("加锁中:" + ClassLayout.parseInstance(obj).toPrintable()); } System.out.println("解锁后:" + ClassLayout.parseInstance(obj).toPrintable()); }
结论:如果只有一个线程t1竞争锁,此时为偏向锁,只会更改对象mark word中线程id,释放锁后,线程id不会清空,当下次t1再次竞争,发现为t1的线程id,此时直接进入,不会通过cas比较,这样节约资源。
-
第二种情况,两个线程t1和t2先后持有锁,但是不会竞争,一定是t1释放锁后,t2再尝试获取锁:
public static void main (String[] args) throws Exception { Demo obj = new Demo(); System.out.println("t1加锁前:" + ClassLayout.parseInstance(obj).toPrintable()); new Thread(() -> { synchronized (obj) { //这里就不打印了,主要观察t2线程的状态 } System.out.println("===============================> t1已经释放锁了"); }, "t1").start(); new Thread(() -> { try { Thread.sleep(5000); //这里休眠5秒,一定要等t1释放锁后再去获取锁,不能产生竞争锁的情况 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2加锁前:" + ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj) { System.out.println("t2加锁中:" + ClassLayout.parseInstance(obj).toPrintable()); } System.out.println("t2解锁后:" + ClassLayout.parseInstance(obj).toPrintable()); }, "t2").start(); }
结论:t1线程先持有锁,加锁前和加锁中和第一点相同,但是当t1释放锁后,t2获取锁时,此时发现线程id已经是t1的,但是不知道偏向锁是否被释放,所以此时必须升级为轻量级锁,锁的后两位改为00,表示轻量级锁,前面位数代表lock record的地址,当t2释放锁后,对象mark word恢复最原始状态。
-
第三种,两个线程不是先后了,是同时竞争锁,这里就不贴图了,直接说下结论:
结论:刚开始对象中仍然是偏向锁,t1持有锁时,改为t1的id,此时t1还没有释放锁,t2开始竞争锁,此时锁膨胀为重量级锁,Mark Word中状态变为10,地址为Monitor的地址,t2进入阻塞队列中等待,t1释放锁,t2获取锁并且释放后,Mark Word恢复成无锁状态,和第二点相同。
-
第四种讲一下批量重定向,和第二种情况相同,对象从偏向锁->轻量级锁->无锁,这样同一个对象先后被两个线程持有锁,此时锁的状态会发生上述的变化,这种情况比较消耗资源,假如现在有19个对象都是发生上述情况,那么从20个对象开始,加锁前还是获取的t1线程id,加锁中就把线程id直接改为t2的线程id,释放锁后也不会改变,剩下的对象都是同样的步骤,此时就相当于批量替换为t2的线程id。
-
第五种:批量撤销,第四种讲了同一个对象被两个线程先后持有锁,会发生偏向锁撤销的情况,如果有多个对象发生这种情况,当阈值达到20的时候剩下的对象会在持有锁后改为t2线程的id。但是这时如果有第三个线程再持有锁,此时前面19个对象变化为
无锁->轻量级锁->无锁
,但是从第20个对象开始,在持有锁前Mark word为t2的id,状态为偏向锁,这时候持有锁后会继续发生偏向锁撤销的情况,在第39个对象偏向锁撤销后,这个类创建的对象仍然是偏向锁,从第40个对象开始,因为偏向锁撤销次数太多,这个类创建的对象不再是偏向锁状态了,而直接是无锁状态。下面张贴代码://为了使结果更直观,我把返回的对象内存信息的Mark Word的二进制提取出来, //顺序做了颠倒,可以从左往右看 public static String parse(String str) { String s = str.split("\r\n")[2].substring(76, 111); String[] arr = s.split(" "); String res = ""; for (int i = arr.length - 1; i >= 0; i--) { res += arr[i] + " "; } return res; }
public static void main (String[] args) throws Exception { List<TwoDemo> list = new ArrayList<>(); int num =38; //对象的个数 new Thread(() -> { for (int i = 0; i < num; i++) { TwoDemo obj = new TwoDemo(); System.out.println("[t1][" + i + "]" + parse(ClassLayout.parseInstance(obj).toPrintable())); synchronized (obj) { System.out.println("[t1][" + i + "]" + parse(ClassLayout.parseInstance(obj).toPrintable())); } System.out.println("[t1][" + i + "]" + parse(ClassLayout.parseInstance(obj).toPrintable())); list.add(obj); } System.out.println("========================>t1线程给批量上锁结束"); }).start(); new Thread(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i < num; i++) { TwoDemo obj = list.get(i); System.out.println("[t2][" + i + "]" + parse(ClassLayout.parseInstance(obj).toPrintable())); synchronized (obj) { System.out.println("[t2][" + i + "]" + parse(ClassLayout.parseInstance(obj).toPrintable())); } System.out.println("[t2][" + i + "]" + parse(ClassLayout.parseInstance(obj).toPrintable())); list.add(obj); } System.out.println("========================>t2线程给批量上锁结束"); }).start(); new Thread(() -> { try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i < num; i++) { TwoDemo obj = list.get(i); System.out.println("[t3][" + i + "]" + parse(ClassLayout.parseInstance(obj).toPrintable())); synchronized (obj) { System.out.println("[t3][" + i + "]" + parse(ClassLayout.parseInstance(obj).toPrintable())); } System.out.println("[t3][" + i + "]" + parse(ClassLayout.parseInstance(obj).toPrintable())); list.add(obj); } System.out.println("========================>t3线程给批量上锁结束"); }).start(); try { Thread.sleep(10000); //主线程睡10秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("主线程创建的对象:" + parse(ClassLayout.parseInstance(new TwoDemo()).toPrintable())); }
下图是信息的显示介绍:
下面输出结果:
[t1][0]00000000 00000000 00000000 00000101 //默认开启偏向锁,但是没有线程id [t1][0]00011001 11111111 01000000 00000101 //持有锁后,还是偏向锁,但是修改为t1的线程id [t1][0]00011001 11111111 01000000 00000101 //释放锁后,状态不变,以下都是批量给对象上锁 [t1][1]00000000 00000000 00000000 00000101 [t1][1]00011001 11111111 01000000 00000101 [t1][1]00011001 11111111 01000000 00000101 //省略重复的信息 [t1][37]00000000 00000000 00000000 00000101 [t1][37]00011001 11111111 01000000 00000101 [t1][37]00011001 11111111 01000000 00000101 ========================>t1线程给批量上锁结束 [t2][0]00011001 11111111 01000000 00000101 //再让t2线程持有锁,此时获取的还是t1的状态 [t2][0]00011011 01110110 11101111 00101000 //此时撤销偏量级锁,升级为轻量级锁,前面改为线程的lock record地址信息 [t2][0]00000000 00000000 00000000 00000001 //释放锁后,对象转为无锁状态 [t2][1]00011001 11111111 01000000 00000101 [t2][1]00011011 01110110 11101111 00101000 [t2][1]00000000 00000000 00000000 00000001 //省略重复的信息 [t2][18]00011001 11111111 01000000 00000101 [t2][18]00011011 01110110 11101111 00101000 [t2][18]00000000 00000000 00000000 00000001 [t2][19]00011001 11111111 01000000 00000101 [t2][19]00011010 11100101 10100001 00000101 //从第20个对象开始,不再发生偏向锁撤销,而是将线程id改为t2的id [t2][19]00011010 11100101 10100001 00000101 //释放锁后,仍然是t2的信息,此时撤销偏向锁次数为19次 [t2][20]00011001 11111111 01000000 00000101 [t2][20]00011010 11100101 10100001 00000101 [t2][20]00011010 11100101 10100001 00000101 //省略重复的信息 [t2][37]00011001 11111111 01000000 00000101 [t2][37]00011010 11100101 10100001 00000101 [t2][37]00011010 11100101 10100001 00000101 ========================>t2线程给批量上锁结束 [t3][0]00000000 00000000 00000000 00000001 //t3线程刚开始获取的还是集合中发生偏向锁撤销的对象,此时状态为无锁 [t3][0]00011011 10000110 11101110 00001000 //直接从无锁升级为轻量级锁 [t3][0]00000000 00000000 00000000 00000001 //释放锁后,变为无锁状态,后面19个对象都是一样 //省略重复的信息 [t3][18]00000000 00000000 00000000 00000001 [t3][18]00011011 10000110 11101110 00001000 [t3][18]00000000 00000000 00000000 00000001 [t3][19]00011010 11100101 10100001 00000101 //第20个对象开始,获取到的是t2线程的信息,此时为偏向锁状态 [t3][19]00011011 10000110 11101110 00001000 //此时会撤销偏向锁,升级为轻量级锁 [t3][19]00000000 00000000 00000000 00000001 //释放锁后,转为无锁状态 //省略重复的信息 [t3][37]00011010 11100101 10100001 00000101 [t3][37]00011011 10000110 11101110 00001000 [t3][37]00000000 00000000 00000000 00000001 ========================>t3线程给批量上锁结束 //t2和t3线程总共加起来发生了38次偏向锁撤销,此时主线程创建对象的时候仍然为默认的偏向锁 主线程创建的对象:00000000 00000000 00000001 00000101
此时把代码中num值改为39,使撤销偏向锁的次数达到39次,再查看主线程创建的对象信息:
//省略以上的信息,发现此时主线程中新创建的对象状态就是无锁状态了。 主线程创建的对象:00000000 00000000 00000000 00000001
结论:
当关闭了偏向锁延迟时,当偏向锁撤销次数达到39次,那么这个类之后新创建的对象不再是偏向锁状态,而是无锁状态,(之前已经创建过的对象仍然保持原来的状态,只是后面用到的时候才会更改状态)。