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

需要关注的点:

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

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

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

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

public class SynchronizedTest {
public static void main(String[] args) throws Exception {
// 直接休眠5秒,或者用-XX:BiasedLockingStartupDelay=0关闭偏向锁延迟
Thread.sleep(5000);
// 反射获取sun.misc的Unsafe对象,用来查看锁的对象头的信息
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
final Unsafe unsafe = (Unsafe) theUnsafe.get(null); // 锁对象
final Object lock = new Object();
// TODO 64位JDK对象头为 64bit = 8Byte,如果是32位JDK则需要换成unsafe.getInt
printf("1_无锁状态:" + getLongBinaryString(unsafe.getLong(lock, 0L))); // 如果不执行hashCode方法,则对象头的中的hashCode为0,
// 但是如果执行了hashCode(identity hashcode,重载过的hashCode方法则不受影响),会导致偏向锁的标识位变为0(不可偏向状态),
// 且后续的加锁不会走偏向锁而是直接到轻量级锁(被hash的对象不可被用作偏向锁)
// lock.hashCode();
// printf("锁对象hash:" + getLongBinaryString(lock.hashCode())); printf("2_无锁状态:" + getLongBinaryString(unsafe.getLong(lock, 0L))); printf("主线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
printf("主线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
// 无锁 --> 偏向锁
new Thread(() -> {
synchronized (lock) {
printf("3_偏向锁:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
printf("偏向线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
printf("偏向线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
// 如果锁对象已经进入了偏向状态,再调用hashCode(),会导致锁直接膨胀为重量级锁
// lock.hashCode();
}
// 再次进入同步快,lock锁还是偏向当前线程
synchronized (lock) {
printf("4_偏向锁:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
printf("偏向线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
printf("偏向线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
}
}).start();
Thread.sleep(1000); // 可以看到就算偏向的线程结束,锁对象的偏向锁也不会自动撤销
printf("5_偏向线程结束:" +getLongBinaryString(unsafe.getLong(lock, 0L)) + "\n"); // 偏向锁 --> 轻量级锁
synchronized (lock) {
// 对象头为:指向线程栈中的锁记录指针
printf("6_轻量级锁:" + getLongBinaryString(unsafe.getLong(lock, 0L)));
// 这里获得轻量级锁的线程是主线程
printf("轻量级线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
printf("轻量级线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
}
new Thread(() -> {
synchronized (lock) {
printf("7_轻量级锁:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
printf("轻量级线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
printf("轻量级线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
}
}).start();
Thread.sleep(1000); // 轻量级锁 --> 重量级锁
synchronized (lock) {
int i = 123;
// 注意:6_轻量级锁 和 8_轻量级锁 的对象头是一样的,证明线程释放锁后,栈帧中的锁记录并未清除,如果方法返回,锁记录是否保留还是清除?
printf("8_轻量级锁:" + getLongBinaryString(unsafe.getLong(lock, 0L)));
// 在锁已经获取了lock的轻量级锁的情况下,子线程来获取锁,则锁会膨胀为重量级锁
new Thread(() -> {
synchronized (lock) {
printf("9_重量级锁:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
printf("重量级线程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
printf("重量级线程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
}
}).start();
// 同步块中睡眠1秒,不会释放锁,等待子线程请求锁失败导致锁膨胀(见轻量级加锁过程)
Thread.sleep(1000);
}
Thread.sleep(500);
} private static String getLongBinaryString(long num) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 64; i++) {
if ((num & 1) == 1) {
sb.append(1);
} else {
sb.append(0);
}
num = num >> 1;
}
return sb.reverse().toString();
}
private static void printf(String str) {
System.out.printf("%s%n", str);
}
}

运行结果如下:

1_无锁状态:0000000000000000000000000000000000000000000000000000000000000101
2_无锁状态:0000000000000000000000000000000000000000000000000000000000000101
主线程hash:0000000000000000000000000000000001001010010101110100011110010101
主线程ID:0000000000000000000000000000000000000000000000000000000000000001 3_偏向锁:0000000000000000000000000000000000011110001001011110100000000101
偏向线程hash:0000000000000000000000000000000001001011010110110100011011111101
偏向线程ID:0000000000000000000000000000000000000000000000000000000000001010 4_偏向锁:0000000000000000000000000000000000011110001001011110100000000101
偏向线程hash:0000000000000000000000000000000001001011010110110100011011111101
偏向线程ID:0000000000000000000000000000000000000000000000000000000000001010 5_偏向线程结束:0000000000000000000000000000000000011110001001011110100000000101 6_轻量级锁:0000000000000000000000000000000000000011000110101111010010110000
轻量级线程hash:0000000000000000000000000000000001001010010101110100011110010101
轻量级线程ID:0000000000000000000000000000000000000000000000000000000000000001 7_轻量级锁:0000000000000000000000000000000000011110101101101111010010001000
轻量级线程hash:0000000000000000000000000000000000011000010110111010100010100100
轻量级线程ID:0000000000000000000000000000000000000000000000000000000000001011 8_轻量级锁:0000000000000000000000000000000000000011000110101111010010110000
9_重量级锁:0000000000000000000000000000000000000011010010101110000100011010
重量级线程hash:0000000000000000000000000000000000111101101111111101111111000111
重量级线程ID:0000000000000000000000000000000000000000000000000000000000001100

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

  • 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,这个问题很奇怪,目前还没找到原因所在。

最新文章

  1. [教程] 【玩转终端1:apt-get】
  2. JAVA 常用框架和工具
  3. 基于Theano的DNN框架Blocks使用简要总结
  4. java web 优化札记
  5. C 本地文件夸网文件Cp操作
  6. C#学习日志 day 3 ------ 基本语句示例
  7. 【转】如何在CentOS/RHEL中安装基于Web的监控系统 linux-das
  8. 代码创建 WPF 旋转动画
  9. [O]SQL SERVER下有序GUID和无序GUID作为主键&amp;聚集索引的性能表现
  10. CentOS7卸载自带jdk安装自己的JDK1.8
  11. 键值编码KVC
  12. vs工具
  13. JS_高程8.BOM window对象(1)
  14. swiper内容滚动
  15. 设计模式学习心得&lt;适配器 Adapter&gt;
  16. Core Expression
  17. HDU 6362(求椭圆中矩形周长的期望 数学)
  18. 全国青少年信息学奥林匹克分区联赛(N)竞赛大纲
  19. [Oracle,2018-01-11] 导出数据库说明文档
  20. java struts2入门学习--防止表单重复提交.OGNL语言学习

热门文章

  1. linux日常管理-linux日志
  2. 大数据学习路线copy自淘宝
  3. javaIO 流分析总结
  4. route-显示并设置Linux内核中的网络路由表
  5. badblocks 检查磁盘损坏的区块
  6. Hander----使用
  7. 牛叉之nc命令
  8. Session和cookie有什么区别?
  9. LeetCode第14题:最长公共前缀
  10. ApiDoc 一键生成注释