前言
- 上篇文章咱们理解了synchronized关键字的常见用法、对象头以及证实了一个对象在无锁状态下的对象头markwork局部的前56位存储的是hashcode。接下来,咱们持续来依据对象头别离证实分代年龄为什么是15、无锁、偏差锁、轻量锁、重(chong)偏差、重(chong)轻量、分量锁,这些锁是实在存在的,咱们能够通过代码来重现。废话不多说,咱们一一来证实
一、证实分代年龄为什么为15
- 大家都晓得,在jvm中,若一个对象在survivor区通过了15次的young gc。当再进行一次young gc时,这个对象将会挪动到老年代。那么为什么是15而不是16、17、18呢?这个问题就跟hashmap的初始容量为什么为16的起因有点类似,都波及到对象的二进制。咱们持续拿java对象头来阐明,请看下图:
由第一张图可知,分代年龄占用了4bit,设想一下,4bit能示意的最大数是什么?没错,就是所有的bit位都是1,即1111
。而二进制的1111
转化成十进制后的值就是15
啦。当初能明确分代年龄为什么是15了吧?
利用此局部,咱们把图中形容的锁状态以表格的形式出现进去
锁状态 锁标识 备注 无锁 001 对象头中应用baised_lock + lock 一共3bit来示意无锁和偏差锁的 偏差锁 101 对象头中应用baised_lock + lock 一共3bit来示意无锁和偏差锁的 轻量锁 00 只用到了lock标识位 分量锁 10 只用到了lock标识位 GC标记 11 只用到了lock标识位
二、证实对象处于无锁状态
- 要证实这个很简略,间接创立一个object对象,并且应用jol打印进去对象头就能剖析出,请细看如下代码及运行后果
第一步:创立User.java类
package com.eugene.basic.concurrency.objectheader;public class User {}
第二步:应用JOL API查看user对象的布局信息
package com.eugene.basic.concurrency.objectheader;import org.openjdk.jol.info.ClassLayout;/** * 验证对象头hashCode信息 */public class Valid { public static void main(String[] args) { User user = new User(); System.out.println(ClassLayout.parseInstance(user).toPrintable()); }}
- 运行后果如下图所示:
上篇文章说了,自己电脑的cpu存储内存是以小端模式存储的,即低位内存存储低位数据。所以咱们只须要看红色框框的第一行的数据。第一行数据看哪呢?看value
局部,它的值为00000001
。依据咱们的java对象头结构图可知,从右边开始数,第1个bit是unused局部、第2-5个bit是分代年龄局部、第6个bit是biased_lock偏差锁标识、第7-8个bit是lock标识。由上剖析可知:最初两位的值为01
,而01
可能代表为无锁或者偏差锁,此时咱们再往前看一位,发现biased_lock位的值为0.因而最初三位值为001
⇒ 证实user对象此时是无锁状态。
三、证实偏差锁
- 证实偏差锁之前,咱们按下图操作,给jvm增加查看全局配置的参数:
间接运行main办法,运行后果如下所示(因为篇幅问题,只截图了要害局部)
由图中的-XX:BiasedLockingStartupDelay=4000配置可知,jvm会在启动虚拟机之后的4s后才会开启偏差锁性能。晓得这个概念后,咱们再来科普下什么是偏差锁。 - 所谓偏差锁:即当一把锁处于可偏差状态时,当有线程持有这把锁后,这把锁将偏差于这个线程。这里提到了可偏差状态,何为可偏差状态呢?可偏差状态是指在jvm开启可偏差性能后,new进去的一个对象它都是可偏差状态,即它的标识位为
101
,然而没有具体的偏差某一个线程。 证实可偏差状态和偏差锁:
增加如下代码并执行:
public class Valid { public static void main(String[] args) throws InterruptedException { // 这里要留神, 肯定要在创建对象之前睡眠,若咱们先创建对象,能够想一想会产生什么状况! // 那必定是不会启动偏差锁的性能呀,咱们都晓得加锁其实是给对象加了个标识 // 如果咱们在偏差锁性能未开启之前创立了对象,很道歉, // jvm没有那么智能,前面不会去把这个对象改成可偏差状态(是偏差锁,然而没有偏差具体 // 的线程) Thread.sleep(4100); System.out.println(ByteOrder.nativeOrder().toString()); User user = new User(); System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); synchronized (user) { System.out.println("lock ing"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } System.out.println("after lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); }}
> 查看运行后果
四、证实一个对象调用了hashcode办法后无奈再被标识为偏差锁,而是升级成轻量锁
编写如下代码(绝对于上述代码,仅在加锁前调用了对象的hashcode办法):
public class Valid { public static void main(String[] args) throws InterruptedException { // 这里要留神, 肯定要在创建对象之前睡眠,若咱们先创建对象,能够想一想会产生什么状况! // 那必定是不会启动偏差锁的性能呀,咱们都晓得加锁其实是给对象加了个标识 // 如果咱们在偏差锁性能未开启之前创立了对象,很道歉, // jvm没有那么智能,前面不会去把这个对象改成可偏差状态(是偏差锁,然而没有偏差具体 // 的线程) Thread.sleep(4100); System.out.println(ByteOrder.nativeOrder().toString()); User user = new User(); System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); System.out.println(user.hashCode()); synchronized (user) { System.out.println("lock ing"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } System.out.println("after lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); }}
运行后果及剖析
五、证实轻量锁
- 这里说下轻量锁的概念:若线程是交替执行的,即上一个线程执行完开释锁后下一个线程再获取锁。若在jvm未开启偏差锁的过程中,对对象进行加锁时,对象间接是轻量锁。
撰写如下代码并执行:
public class Valid { public static void main(String[] args) throws InterruptedException { System.out.println(ByteOrder.nativeOrder().toString()); User user = new User(); System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); synchronized (user) { System.out.println("lock ing"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } System.out.println("after lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); }}
运行后果如下
六、证实偏差锁收缩为轻量锁
编写如下代码并执行
public class Valid { public static void main(String[] args) throws InterruptedException { // 开启偏差锁性能 Thread.sleep(4100); System.out.println(ByteOrder.nativeOrder().toString()); User user = new User(); System.out.println("before lock" + ClassLayout.parseInstance(user).toPrintable()); synchronized (user) { System.out.println("lock ing" + ClassLayout.parseInstance(user).toPrintable()); } System.out.println("after lock" + ClassLayout.parseInstance(user).toPrintable()); // 开启线程来获取锁 Thread t1 = new Thread(() -> { synchronized (user) { System.out.println("other t1 thread get lock" + ClassLayout.parseInstance(user).toPrintable()); } }, "t1"); t1.start(); // 期待t1执行完后再打印一次锁信息 t1.join(); System.out.println("after t1 thread release lock" + ClassLayout.parseInstance(user).toPrintable()); }}
运行后果及剖析
七、证实重(chong)偏差
- 持续援用第三章:证实偏差锁的图
咱们关注intx BiasedLockingBulkRebiasThreshold = 20
此配置。此配置阐明整个偏差锁重偏差的阈值为20。ok,阈值咱们晓得了,接下来阐明下什么叫做重偏差 - 所谓重偏差,依照字面意思来了解就是:锁的重偏差过程。然而大家都晓得,锁的状态是不可逆的,当偏差锁被其余线程持有后就会收缩成轻量锁了。然而,这里的重偏差是指批量重偏差。咱们先来看例子再来总结:
编写如下类,并运行它:
public class ReBiasedLock { static List<User> locks = new ArrayList<>(); static final int THREAD_COUNT = 19; public static void main(String[] args) throws InterruptedException { // 提早4.1秒,期待jvm偏差锁性能开启 Thread.sleep(4300); // 线程1中的锁全为偏差锁。 Thread t1 = new Thread(() -> { for (int i = 0; i < THREAD_COUNT; i++) { User lock = new User(); locks.add(lock); synchronized (lock) { System.out.println("线程1 第 " + (i + 1) + " 把锁"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); System.out.println("\n *********************************** \n"); } } }, "线程1"); t1.start(); // 等t1执行完 t1.join(); // 增加一个新线程,防止出现偏差锁的id反复的状况 // 我也不晓得为什么,只晓得这样能解决这样的问题 Thread tmp = new Thread(() -> { System.out.println(1); }, "tmp"); tmp.start(); new Thread(() -> { for (int i = 0; i < locks.size(); i++) { User lock = locks.get(i); synchronized (lock) { System.out.println("线程2 第 " + (i + 1) + " 把锁"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); System.out.println("\n ==================================== \n"); } } }, "线程2").start(); }}
> **当线程数量THREAD_COUNT=19时**,第一个循环执行结束后,线程list中的user对象全副为偏差锁,偏差于线程1。第二个线程执行结束后,list中的user对象全副收缩成轻量锁。这里查看下第一次和第二次循环的局部输入
咱们测试另一种状况,把THREAD_COUNT改成25并执行它
> **当线程数量THREAD_COUNT=25时**,第一个循环执行结束后,同上,list中的user对象全副为偏差锁。第二个循环执行完后,前19把锁是轻量锁,从第20把锁开始,及其前面的所有的锁,都变成了偏差锁,从新偏差成了线程2。
- 论断:
当同一类型的锁被同一个线程收缩轻量锁的次数达到了20,那么会将后续的同一类型的锁对立重偏差到以后线程。
八、证实重(chong)轻量
- 持续援用第三章:证实偏差锁的图
咱们关注intx BiasedLockingBulkRevokeThreshold = 40
配置。此配置阐明整个重轻量的阈值为40。ok,阈值咱们晓得了,接下来阐明下什么叫做重轻量 - 重轻量概念:若同一类型的锁降级轻量锁的次数达到了40,此时就会将前面的锁都批量撤销为无锁状态,并收缩到轻量锁
咱们新增如下代码并运行它:
public class ReLightweightLock { static List<User> locks = new ArrayList<>(); public static void main(String[] args) throws InterruptedException { System.out.println("Starting"); // 提早加载,让jvm开启偏差锁性能 Thread.sleep(4400);
Thread t1 = new Thread(() -> { for (int i = 0; i < 45; i++) { User lock = new User(); locks.add(lock); synchronized (lock) { // 不做任何事,能够确定45把锁全副变成了偏差锁 } } }, "t1"); t1.start(); t1.join(); // 打印第43把锁,曾经是偏差锁了 System.out.println("i = 42 \t" + ClassLayout.parseInstance(locks.get(42)).toPrintable()); // 创立一个新线程睡眠2s,保障上面的代码先执行,保障重偏差时,不会呈现线程ID反复的状况 new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }, "tmp1").start(); Thread t2 = new Thread(() -> { for (int i = 0; i < locks.size(); i++) { User lock = locks.get(i); synchronized (lock) { if (i == 10 || i == 21) { // 输入第11和22个,看看别离是不是轻量锁和偏差锁 System.out.println("t2 i = " + i + "\t" + ClassLayout.parseInstance(lock).toPrintable()); } } } }, "t1"); t2.start(); t2.join(); // 查看第11把锁对象,看看是不是20之前的锁也被重偏差了 --> 后果证实,只会对20当前的锁重偏差 // 这里输入的是无锁状态,因为i= 10时,被线程2持有过,收缩成轻量锁了,而轻量锁在开释锁后会变成无锁状态 System.out.println("i = 10\t" + ClassLayout.parseInstance(locks.get(10)).toPrintable()); // 查看第43把锁对象,看看是不是被批量重偏差了 --> 后果证实:是的 System.out.println("i = 42\t" + ClassLayout.parseInstance(locks.get(42)).toPrintable()); // 创立一个新线程睡眠2s,保障上面的代码先执行,保障重偏差时,不会呈现线程ID反复的状况 new Thread(() -> { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }, "tmp2").start(); Thread t3 = new Thread(() -> { for (int i = 0; i < locks.size(); i++) { User lock = locks.get(i); synchronized (lock) { if (i == 10 || i == 21 || i == 40) { // 输入第11和22个,看看是不是都为轻量锁 // ---> 后果证实:都为轻量锁 // i == 10为轻量锁,咱们都能了解,因为偏差锁被其余线程持有了,当然收缩为轻量锁了 // 可是i == 21不应该为偏差锁么?(超过了重偏差的阈值) // ==> 这里不是重偏差了,因为user类型的锁降级为轻量锁的次数达到了40(线程2降级了20次), // 所以jvm间接做了重轻量的操作,把前面所有的锁都变成轻量锁了 // 所以i == 21应该是轻量锁 // i == 40同样也是轻量锁 System.out.println("t3 i = " + i + "\t" + ClassLayout.parseInstance(lock).toPrintable()); } } } }, "t3"); t3.start(); t3.join(); // 此时是无锁状态,因为线程3进行批量重轻量了,而它开释了锁,所以是无锁状态 System.out.println("main i = 40 \t" + ClassLayout.parseInstance(locks.get(40)).toPrintable()); }}```剖析后果在正文上曾经有了,能够依据正文信息和上面的运行后果来做比对```txtStartingi = 42 com.eugene.basic.concurrency.objectheader.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 f8 a1 29 (00000101 11111000 10100001 00101001) (698480645) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totalt2 i = 10 com.eugene.basic.concurrency.objectheader.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) e8 f5 69 2a (11101000 11110101 01101001 00101010) (711587304) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totalt2 i = 21 com.eugene.basic.concurrency.objectheader.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 59 a8 29 (00000101 01011001 10101000 00101001) (698898693) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totali = 10 com.eugene.basic.concurrency.objectheader.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totali = 42 com.eugene.basic.concurrency.objectheader.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 59 a8 29 (00000101 01011001 10101000 00101001) (698898693) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totalt3 i = 10 com.eugene.basic.concurrency.objectheader.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) c8 ee 89 2a (11001000 11101110 10001001 00101010) (713682632) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totalt3 i = 21 com.eugene.basic.concurrency.objectheader.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) c8 ee 89 2a (11001000 11101110 10001001 00101010) (713682632) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totalt3 i = 40 com.eugene.basic.concurrency.objectheader.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) c8 ee 89 2a (11001000 11101110 10001001 00101010) (713682632) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totalmain i = 40 com.eugene.basic.concurrency.objectheader.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes totalProcess finished with exit code 0```
九、证实分量锁
- 分量锁概念:多个线程存在强烈的竞争时,锁会收缩成分量锁,且不可逆!
典型案例:生产者消费者模型:
public class ValidSynchronized { static Object lock = new Object(); static volatile LinkedList<String> queue = new LinkedList<>(); public static void main(String[] args) throws InterruptedException { System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); Consumer consumer = new Consumer(); Producer producer = new Producer(); consumer.start(); producer.start(); Thread.sleep(500); consumer.interrupt(); producer.interrupt(); // 睡眠3s ==> 目标是为了让锁本人开释,避免在开释过程中打印锁的状态呈现分量锁的状况 Thread.sleep(3000); System.out.println("after lock"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); }}class Producer extends Thread { @Override public void run() { while (!isInterrupted()) { synchronized (ValidSynchronized.lock) { System.out.println("lock ing"); System.out.println(ClassLayout.parseInstance(ValidSynchronized.lock).toPrintable()); String message = UUID.randomUUID().toString(); System.out.println("生产者生产音讯:" + message); ValidSynchronized.queue.offer(message); try { // 生产者本人wait,目标是开释锁 ValidSynchronized.lock.notify(); ValidSynchronized.lock.wait(); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { this.interrupt(); } } } }}class Consumer extends Thread { @Override public void run() { while (!isInterrupted()) { synchronized (ValidSynchronized.lock) { if (ValidSynchronized.queue.size() == 0) { try { ValidSynchronized.lock.wait(); ValidSynchronized.lock.notify(); } catch (InterruptedException e) { e.printStackTrace(); } } String message = ValidSynchronized.queue.pollLast(); System.out.println("消费者生产音讯:" + message); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { this.interrupt(); } } } }}
运行后果:
十、证实调用wait办法后,锁会降级为分量锁
运行如下代码:
public class ValidWait { public static void main(String[] args) throws InterruptedException { Thread.sleep(4100); final User user = new User(); System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); Thread t1 = new Thread(() -> { synchronized (user) { System.out.println("lock ing"); System.out.println("before wait"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); try { user.wait(); System.out.println("after wait"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t1"); t1.start(); // 主线程睡眠3s后,唤醒t1线程 Thread.sleep(3000); System.out.println("主线程查看锁,变成了分量锁"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); }}
运行后果如下
十一、总结
- 偏差锁和hashcode是互斥的,只能存在一个。
- jvm默认对偏差锁性能是提早加载的,大略工夫为4s钟,能够增加JVM参数:
-XX:BiasedLockingStartupDelay=0
来设置延迟时间为0。偏差锁的提早加载敞开后,基本上所有的锁都会为可偏差状态,即mark word为101,然而它还没有具体偏差的线程信息 - 偏差锁退出同步块后仍然也是偏差锁
- 重量级锁之所以分量就是因为状态不停的切换,最终映射到代码层面就是不停的调用操作系统函数(最终会调用到jvm的
mutex
类) - 调用锁对象的wait办法时,以后锁对象会立马降级为重量级锁
- 偏差锁只有被其余线程拿到了,此时偏差锁会收缩。收缩为轻量锁
- 并发模块对应github地址:传送门
- I am a slow walker, but I never walk backwards.