并发编程:synchronized 锁升级过程的验证
关于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,这个问题很奇怪,目前还没找到原因所在。