关于synchronized关键字以及偏向锁、轻量级锁、重量级锁的介绍广大网友已经给出了太多文章和例子,这里就不再重复了,也可点击链接来回顾一下。在这里来实战操作一把,验证JVM是怎么一步一步对锁进行升级的,这其中有很多值得思考的地方。

需要关注的点:

  • JDK8偏向锁默认是开启的,不过JVM启动后有4秒钟的延迟,所以在这4秒钟内对家加锁都直接是轻量级锁,可用-XX:BiasedLockingStartupDelay=0 关闭该特性

  • 测试用的JDK是64位的,所以获取对象头的时候是用unsafe.getLong,来获取对象头Markword的8个字节,如果你是32位则用unsafe.getInt替换即可

  • hashCode方法会对偏向锁造成影响(这里的hashCode特指identity hashcode,如果锁对象重载过hashCode方法则不会影响)

剩下的,我们直接代码里来相见:

  1. public class SynchronizedTest {
  2. public static void main(String[] args) throws Exception {
  3. // 直接休眠5秒,或者用-XX:BiasedLockingStartupDelay=0关闭偏向锁延迟
  4. Thread.sleep(5000);
  5. // 反射获取sun.misc的Unsafe对象,用来查看锁的对象头的信息
  6. Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
  7. theUnsafe.setAccessible(true);
  8. final Unsafe unsafe = (Unsafe) theUnsafe.get(null);
  9. // 锁对象
  10. final Object lock = new Object();
  11. // TODO 64位JDK对象头为 64bit = 8Byte,如果是32位JDK则需要换成unsafe.getInt
  12. printf("1_无锁状态:" + getLongBinaryString(unsafe.getLong(lock, 0L)));
  13. // 如果不执行hashCode方法,则对象头的中的hashCode为0,
  14. // 但是如果执行了hashCode(identity hashcode,重载过的hashCode方法则不受影响),会导致偏向锁的标识位变为0(不可偏向状态),
  15. // 且后续的加锁不会走偏向锁而是直接到轻量级锁(被hash的对象不可被用作偏向锁)
  16. // lock.hashCode();
  17. // printf("锁对象hash:" + getLongBinaryString(lock.hashCode()));
  18. printf("2_无锁状态:" + getLongBinaryString(unsafe.getLong(lock, 0L)));
  19. printf("主线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
  20. printf("主线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
  21. // 无锁 --> 偏向锁
  22. new Thread(() -> {
  23. synchronized (lock) {
  24. printf("3_偏向锁:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
  25. printf("偏向线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
  26. printf("偏向线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
  27. // 如果锁对象已经进入了偏向状态,再调用hashCode(),会导致锁直接膨胀为重量级锁
  28. // lock.hashCode();
  29. }
  30. // 再次进入同步快,lock锁还是偏向当前线程
  31. synchronized (lock) {
  32. printf("4_偏向锁:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
  33. printf("偏向线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
  34. printf("偏向线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
  35. }
  36. }).start();
  37. Thread.sleep(1000);
  38. // 可以看到就算偏向的线程结束,锁对象的偏向锁也不会自动撤销
  39. printf("5_偏向线程结束:" +getLongBinaryString(unsafe.getLong(lock, 0L)) + "\n");
  40. // 偏向锁 --> 轻量级锁
  41. synchronized (lock) {
  42. // 对象头为:指向线程栈中的锁记录指针
  43. printf("6_轻量级锁:" + getLongBinaryString(unsafe.getLong(lock, 0L)));
  44. // 这里获得轻量级锁的线程是主线程
  45. printf("轻量级线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
  46. printf("轻量级线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
  47. }
  48. new Thread(() -> {
  49. synchronized (lock) {
  50. printf("7_轻量级锁:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
  51. printf("轻量级线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
  52. printf("轻量级线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
  53. }
  54. }).start();
  55. Thread.sleep(1000);
  56. // 轻量级锁 --> 重量级锁
  57. synchronized (lock) {
  58. int i = 123;
  59. // 注意:6_轻量级锁 和 8_轻量级锁 的对象头是一样的,证明线程释放锁后,栈帧中的锁记录并未清除,如果方法返回,锁记录是否保留还是清除?
  60. printf("8_轻量级锁:" + getLongBinaryString(unsafe.getLong(lock, 0L)));
  61. // 在锁已经获取了lock的轻量级锁的情况下,子线程来获取锁,则锁会膨胀为重量级锁
  62. new Thread(() -> {
  63. synchronized (lock) {
  64. printf("9_重量级锁:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
  65. printf("重量级线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
  66. printf("重量级线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
  67. }
  68. }).start();
  69. // 同步块中睡眠1秒,不会释放锁,等待子线程请求锁失败导致锁膨胀(见轻量级加锁过程)
  70. Thread.sleep(1000);
  71. }
  72. Thread.sleep(500);
  73. }
  74. private static String getLongBinaryString(long num) {
  75. StringBuilder sb = new StringBuilder();
  76. for (int i = 0; i < 64; i++) {
  77. if ((num & 1) == 1) {
  78. sb.append(1);
  79. } else {
  80. sb.append(0);
  81. }
  82. num = num >> 1;
  83. }
  84. return sb.reverse().toString();
  85. }
  86. private static void printf(String str) {
  87. System.out.printf("%s%n", str);
  88. }
  89. }

运行结果如下:

  1. 1_无锁状态:0000000000000000000000000000000000000000000000000000000000000101
  2. 2_无锁状态:0000000000000000000000000000000000000000000000000000000000000101
  3. 主线程hash0000000000000000000000000000000001001010010101110100011110010101
  4. 主线程ID0000000000000000000000000000000000000000000000000000000000000001
  5. 3_偏向锁:0000000000000000000000000000000000011110001001011110100000000101
  6. 偏向线程hash0000000000000000000000000000000001001011010110110100011011111101
  7. 偏向线程ID0000000000000000000000000000000000000000000000000000000000001010
  8. 4_偏向锁:0000000000000000000000000000000000011110001001011110100000000101
  9. 偏向线程hash0000000000000000000000000000000001001011010110110100011011111101
  10. 偏向线程ID0000000000000000000000000000000000000000000000000000000000001010
  11. 5_偏向线程结束:0000000000000000000000000000000000011110001001011110100000000101
  12. 6_轻量级锁:0000000000000000000000000000000000000011000110101111010010110000
  13. 轻量级线程hash0000000000000000000000000000000001001010010101110100011110010101
  14. 轻量级线程ID0000000000000000000000000000000000000000000000000000000000000001
  15. 7_轻量级锁:0000000000000000000000000000000000011110101101101111010010001000
  16. 轻量级线程hash0000000000000000000000000000000000011000010110111010100010100100
  17. 轻量级线程ID0000000000000000000000000000000000000000000000000000000000001011
  18. 8_轻量级锁:0000000000000000000000000000000000000011000110101111010010110000
  19. 9_重量级锁:0000000000000000000000000000000000000011010010101110000100011010
  20. 重量级线程hash0000000000000000000000000000000000111101101111111101111111000111
  21. 重量级线程ID0000000000000000000000000000000000000000000000000000000000001100

现在依此来看下各个状态:

  • 1_无锁状态:通过结果可以看到:对象的hashCode为0,gc分代年龄也是0,偏向锁标志位为1(表示可偏向状态),锁标志位为01

  • 2_无锁状态:如果不执行hashCode方法,则跟1_无锁状态一致,否则为:0000000000000000000000000100101001010111010001111001010100000001

    偏向锁标志位为0,表示不可偏向状态,这里网友们大多有误解,实际应该为:偏向锁标志位表示的是当前锁是否可偏向

  • 3_偏向锁:子线程首次获取锁,则锁偏向子线程

  • 4_偏向锁:子线程是否锁后再次获取锁,JVM检测到锁是偏向子线程的,所以直接获取

  • 5_偏向线程结束:偏向的线程结束后,锁对象的对象头没有改变,所以偏向锁也不会自动撤销(这里JDK团队是否可以做优化呢?还是说线程根本就没记录哪些锁偏向了自己,所以退出的时候也没法一一撤销)

  • 6_轻量级锁:如果锁已经偏向了一个线程,则其他现在来获取锁,则需要升级为轻量级锁

  • 7_轻量级锁:只要没有多个线程同一时刻来竞争锁,则多个线程可以轮流使用这把轻量级锁(使用完后会及时释放,CAS替换Markword)

  • 8_轻量级锁、9_重量级锁:主线程先获取轻量级锁,在持有锁的同时,创建一个子线程来获取同一把锁,这时候有了锁的竞争,则会升级为重量级锁

注意:

如果把代码里的第一行或者第二行lock.hashCode();注释掉的话,则执行的结果完全就不同了,也可从结果验证上文提到的hashCode对偏向锁的影响。

还剩一个问题:

网上经常能看到的一张对象头布局图,其中偏向锁状态时Markword存储的是:线程ID + Epoch + 分代年龄 + 1 + 01





但是,我在程序中验证了,锁对象处于偏向锁的状态时,Markword存储的内容既不是线程ID也不是线程对象的hashCode,这个问题很奇怪,目前还没找到原因所在。

并发编程:synchronized 锁升级过程的验证的更多相关文章

  1. 详细了解 synchronized 锁升级过程

    前言 首先,synchronized 是什么?我们需要明确的给个定义--同步锁,没错,它就是把锁. 可以用来干嘛?锁,当然当然是用于线程间的同步,以及保护临界区内的资源.我们知道,锁是个非常笼统的概念 ...

  2. 关于Synchronized的偏向锁,轻量级锁,重量级锁,锁升级过程,自旋优化,你该了解这些

    前言 相信大部分开发人员,或多或少都看过或写过并发编程的代码.并发关键字除了Synchronized(如有不懂请移至传送门,关于Synchronized的偏向锁,轻量级锁,重量级锁,锁升级过程,自旋优 ...

  3. Synchronized锁升级原理与过程深入剖析

    Synchronized锁升级原理与过程深入剖析 前言 在上篇文章深入学习Synchronized各种使用方法当中我们仔细介绍了在各种情况下该如何使用synchronized关键字.因为在我们写的程序 ...

  4. synchronized锁升级详细过程

    java对象头由3部分组成: 1.Mark Word 2.指向类对象(对象的class对象)的指针 3.数组长度(数组类型才有) 重点是 Mark Word结构,下面以32位HotSpot为例: 一. ...

  5. 再谈synchronized锁升级

    在图文详解Java对象内存布局这篇文章中,在研究对象头时我们了解了synchronized锁升级的过程,由于篇幅有限,对锁升级的过程介绍的比较简略,本文在上一篇的基础上,来详细研究一下锁升级的过程以及 ...

  6. Java并发编程:锁的释放

    Java并发编程:锁的释放 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: #839496;} Ja ...

  7. 5.并发编程-synchronized 细节说明

    并发编程-synchronized 细节说明 1. synchronized-锁重入 & 异常释放锁 说明 * 关键字synchronized 拥有锁重入的功能,也就是在使用synchroni ...

  8. Synchronized锁升级

    Synchronized锁升级 锁的4中状态:无锁状态.偏向锁状态.轻量级锁状态.重量级锁状态(级别从低到高) 为什么要引入偏向锁? 因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞 ...

  9. 深入并发锁,解析Synchronized锁升级

    这篇文章分为六个部分,不同特性的锁分类,并发锁的不同设计,Synchronized中的锁升级,ReentrantLock和ReadWriteLock的应用,帮助你梳理 Java 并发锁及相关的操作. ...

随机推荐

  1. java 正则表达式 验证邮箱

    import java.util.regex.Matcher; import java.util.regex.Pattern; public class demo1 { /**java正则表达式 * ...

  2. hbase->Mapreduce->hbase

    Hbase对Mapreduce API进行了扩展,方便Mapreduce任务读写HTable数据. package taglib.customer; import java.io.IOExceptio ...

  3. hdu1080

    #include<iostream> using namespace std; char s1[105],s2[105]; int val[5][5]={ {5,-1,-2,-1,-3}, ...

  4. redis系列:通过文章点赞排名案例学习sortedset命令

    前言 这一篇文章将讲述Redis中的sortedset类型命令,同样也是通过demo来讲述,其他部分这里就不在赘述了. 项目Github地址:https://github.com/rainbowda/ ...

  5. 引用静态资源的url添加版本号,解决版本发布后的浏览器缓存有关问题

    在日常的工作中,我们经常会遇到页面文件(html,jsp等)中引用的js,css,图片等被修改后,而浏览器依然缓存着老版本的文件,客户一时半会看不到修改后的效果,同时也给生产环境的版本发布带来了一些问 ...

  6. CodeForces 116B【二分匹配】

    思路: 暴力..我不会呀.. YY一个二分匹配嘛,然后数组开小了.GG for an hour. #include <bits/stdc++.h> using namespace std; ...

  7. Exception异常处理

    1.java异常类: 都是Throwable的子类: 1.Exception(异常) :是程序本身可以处理的异常. 2.Error(错误): 是程序无法处理的错误.这些错误表示故障发生于虚拟机自身.或 ...

  8. JVM调试过程

    一.查看系统情况 Linux查看CPU和内存使用情况 二.查看JVM启动参数 2.1 jcmd JVM诊断之查看运行参数

  9. VBA学习笔记

    这是一个学习VBA编程的学习笔记. 一. 介绍 二. 使用手册 2.1. 如何在Excel2010中开始使用VBA? 2.2. 如何使用VBA编辑器进行编程? 三. 语法说明 3.1 数据类型 3.2 ...

  10. Python中使用Type hinting 和 annotations

    Type hints最大的好处就是易于代码维护.当新成员加入,想要贡献代码时,能减少很多时间. 也方便我们在调用汉书时提供了错误的类型传递导致运行时错误的检测. 第一个类型注解示例 我们使用一个简单例 ...