李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
16.并发编程之synchronized原理进阶(二)
Leefs
2022-10-18 PM
603℃
0条
[TOC] ### 一、偏向锁概念 轻量级锁在**没有竞争**时(就自己这个线程),每次重入仍然需要执行 CAS操作。 Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。 - **升级为轻量级锁的情况 (会进行偏向锁撤销)** : 获取偏向锁的时候, 发现线程ID不是自己的, 此时通过CAS替换操作, 操作成功了, 此时该线程就获得了锁对象。( 此时是交替访问临界区, 撤销偏向锁, 升级为轻量级锁)。 - **升级为重量级锁的情况 (会进行偏向锁撤销)** : 获取偏向锁的时候, 发现线程ID不是自己的, 此时通过CAS替换操作, 操作失败了, 此时说明发生了锁竞争。( 此时是多线程访问临界区, 撤销偏向锁, 升级为重量级锁)。 **示例** ```java static final object obj = new object(); public static void m1() { synchronized( obj ) { //同步块A m2(); } } public static void m2() { synchronized( obj ) { //同步块B m3(); } } public static void m3() { synchronized( obj ) { //同步块C } } ``` ![16.并发编程之synchronized原理进阶01.png](https://lilinchao.com/usr/uploads/2022/10/896859336.png) ![16.并发编程之synchronized原理进阶02.png](https://lilinchao.com/usr/uploads/2022/10/144160451.png) ### 二、偏向状态 ![16.并发编程之synchronized原理进阶03.png](https://lilinchao.com/usr/uploads/2022/10/3268833265.png) **状态说明** | 状态 | 说明 | | ----------- | ------------------------------------------------------------ | | Normal | 一般状态,没有加任何锁,前面62位保存的是对象的信息,最后2位为状态(01),倒数第三位表示是否使用偏向锁(未使用:0) | | Biased | 偏向状态,使用偏向锁,前面54位保存的当前线程的ID,最后2位为状态(01),倒数第三位表示是否使用偏向锁(使用:1) | | Lightweight | 使用轻量级锁,前62位保存的是锁记录的指针,最后2位为状态(00) | | Heavyweight | 使用重量级锁,前62位保存的是Monitor的地址指针,最后2位为状态(10) | **一个对象创建时:** + 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 `0x05` 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0 + 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 `- XX:BiasedLockingStartupDelay=0` 来禁用延迟 + 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值 ##### (1)测试偏向锁 + 利用jol第三方工具来查看对象头信息(注意这里扩展了jol让它输出更为简洁) ```java //添如虚拟机参数-XX:BiasedLockingStartupDelay=0 public static void main(String[] args) throws IOException { Dog d = new Dog(); ClassLayout classLayout = ClassLayout.lparseInstance(d); new Thread(() -> { log.debug("synchronized前"); System.out.println(classLayout.toPrintableSimple(true)); synchronized (d) { log.debug("synchronized中"); System.out.println(classlayout.toPrintableSimple(true)); } log.debug(" synchraoized后"); System.out.println(classLayout.toPrintablesimple(true)); }, "t1").start(); } class Dog{ } ``` **输出结果** ```basic 11:08:58.117 c. TestBiased [t1] - synchronized 前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 11:08:58.121 C. TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 11:08:58.121 C. TestBiased [t1] - synchronized 后 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 ``` > 注意 > > 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中 ##### (2)测试禁用 在上面测试代码运行时在添加 VM 参数 `-XX:-UseBiasedLocking` 禁用偏向锁 **输出结果** ```basic 11:13:10.018 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 11:13:10.021 C. TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 11:13:10.021 C. TestBiased [t1] - synchronized 后 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 ``` 注意:最后三位001表示没有加偏向锁,最后两位00表示加了轻量级锁 说明偏向锁禁用成功! ##### (3)测试hasecode + 正常状态对象一开始是没有 hashCode 的,第一次调用才生成 ```java public static void main(String[] args) throws IOException { Dog d = new Dog(); d.hashcode();//调用对象hashcode,使得偏向锁禁用 ClassLayout classLayout = ClassLayout.lparseInstance(d); new Thread(() -> { log.debug("synchronized前"); System.out.println(classLayout.toPrintableSimple(true)); synchronized (d) { log.debug("synchronized中"); System.out.println(classlayout.toPrintableSimple(true)); } log.debug(" synchraoized后"); System.out.println(classLayout.toPrintablesimple(true)); }, "t1").start(); } ``` ![16.并发编程之synchronized原理进阶03.png](https://lilinchao.com/usr/uploads/2022/10/3268833265.png) 观察如上的MarkWord格式,Normal下的hashcode占31位,Biased下的thread:54位,无法在装下31位的hashcode。所以,**可偏向对象调了hashcode()后撤销偏向状态** > 轻量级锁:hashcode会存到线程栈帧的锁记录(lock Record)中 > > 重量级锁:hashcode会存到monitor对象中 ### 三、偏向锁撤销场景 #### 3.1 调用对象haseCode 调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致偏向锁被撤销 - 轻量级锁会在锁记录中记录hashCode - 重量级锁会在Monitor中记录hashCode 在调用hashCode后使用偏向锁,记得去掉`-XX: -UseBiasedLocking` **输出结果** ```basic 11:22:10.386 c.TestBiased [main] - 调用hashCode: 1778535015 11:22:10.391 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 正常状态,没有偏向锁 11:22:10.393 C. TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000 加了轻量级锁 11:22:10.393 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 撤销 正常状态,没有偏向锁 ``` #### 3.2 其它线程使用对象 **偏向锁、轻量级锁的使用条件, 都是在于多个线程没有对同一个对象进行`锁竞争`的前提下, 如果有`锁竞争`,此时就使用重量级锁。** 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 ```java private static void test2() throws InterruptedException { Dog d = new Dog(); Thread t1 = new Thread(() -> { synchronized (d) { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); } synchronized (TestBiased.class) { TestBiased.class.notify(); } // 如果不用 wait/notify 使用 join 必须打开下面的注释 // 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的 /*try { System.in.read(); } catch (IOException e) { e.printStackTrace(); }*/ }, "t1"); t1.start(); Thread t2 = new Thread(() -> { synchronized (TestBiased.class) { try { TestBiased.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); }, "t2"); t2.start(); } ``` **输出结果** ```basic [t1] - 0000000 00000000 00000000 0000000 00011111 01000001 00010000 00000101 //偏向锁 [t2] - 00000000 00000000 0000000 0000000 00011111 01000001 00010000 00000101 //偏向锁 [t2] - 00000000 0000000 00000000 0000000 00011111 10110101 11110000 01000000 //撤销偏向锁,改为轻量级锁,保留线程id [t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 //恢复正常 ``` #### 3.3 调用wait/notify wait/notify只有重锁才有,任何线程对象调用其时,会升级为**重锁** ```java public static void main(String[] args) throws InterruptedException { Dog d = new Dog(); Thread t1 = new Thread(() -> { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); try { d.wait(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); } }, "t1"); t1.start(); new Thread(() -> { try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (d) { log.debug("notify"); d.notify(); } }, "t2").start(); } ``` **输出结果** ```basic [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 [t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101 [t2] - notify [t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010 ``` ### 四、批量重偏向 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID。 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程 **示例** ```java public class Demo01 { public static void test() { Vector
list = new Vector<>(); Thread t1 = new Thread(() -> { for (int i = 0; i < 30; i++) { Dog d = new Dog(); list.add(d); synchronized (d) { log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } } synchronized (list) { list.notify();//唤醒list } }, "t1"); t1.start(); Thread t2 = new Thread(() -> { synchronized (list) { try { list.wait();//阻塞list,释放锁 } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("===========> "); for (int i = 0; i < 30; i++) { Dog d = list.get(i); log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintablesimple(true)); synchronized (d) { log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintablesimple(true)); log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } } }, "t2"); t2.start(); } } ``` **输出结果** ```basic xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 线程id 线程id 线程id 加锁状态 [t1] - 0 00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 [t1] - 1 00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 [t1] - 2 00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 [t1] - 3 00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 [t1] - 4 00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 [t1] - 5 00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 ...t1 从1到29都是加的线程id(00011111 11101011)偏向锁,状态看最后101 [t2] - ============> [t2] - 0 00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 //原始t1的偏向锁状态 [t2] - 0 00000000 00000000 00000000 00000000 00100000 01111010 11110110 01110000 //撤销偏向锁,升级轻量级锁 [t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001//解锁后,变为不可偏向状态 [t2] - 1 00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 [t2] - 1 00000000 00000000 00000000 00000000 00100000 01111010 11110110 01110000 [t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 ... //我们发现,到了第20个的时候(从0算第1个),又变成了偏向锁状态,但是偏向的id变成了t2了 //之后所有的对象都是直接偏向的状态,而不是先撤销t1偏锁,再升级轻锁 => 批量重偏向 [t2] - 19 00000000 00000000 00000000 00000000 00011111 11101011 01000000 00000101 [t2] - 19 00000000 00000000 00000000 00000000 00011111 11101011 01010001 00000101 [t2] - 19 00000000 00000000 00000000 00000000 00011111 11101011 01010001 00000101 ... ``` ### 五、批量撤销 当撤销偏向锁阈值超过40次后,jvm 会这样觉得,自己确实偏向错了, 根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的 ```java public class Demo02 { static Thread t1, t2, t3; public static void test() { Vector
list = new Vector<>(); int loopNumber = 39; t1 = new Thread(() -> { for (int i = 0; i < loopNumber; i++) { Dog d = new Dog(); list.add(d); //39个对象加上偏向锁,偏向t1线程 synchronized (d) { log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } //39个对象加完锁唤醒t2(park,unpark方式) LockSupport.unpark(t2); } }, "t1"); t1.start(); t2 = new Thread(() -> { LockSupport.park();//先阻塞自己 log.debug("============> "); for (int i = 0; i < loopNumber; i++) { Dog d = list.get(i);//拿出list对象 Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); //对象加上偏向锁,偏向t2线程 //前19个对象是撤销t1偏向锁,之后对象是批量重偏向 synchronized (d) { Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintablesimple(true)); } Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } //此时已经重偏向了20次 LockSupport.unpark(t3);//唤醒t3 }, "t2"); t2.start(); t3 = new Thread(() -> { LockSupport.park();//先阻塞自己 log.debug("============> "); for (int i = 0; i < loopNumber; i++) { Dog d = list.get(i);//拿出list对象 Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); //对象加上偏向锁,偏向t3线程 //前19个对象是撤销t2偏向锁,注意:之后对象也是撤销t2偏锁,没那么多机会重偏向锁了 synchronized (d) { Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintablesimple(true)); } Log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } //最后撤销偏向锁达到39次 }, "t3"); t3.start(); t3.join(); /* 当撤销偏向锁阈值超过40次后,jvm会这样觉得,自己确实偏向错了,根本就不该偏向。 于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的,所以new Dog()是不可偏向的 */ Log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true)); } } ``` ### 六、锁消除 - 线程同步的代价是相当高的,同步的后果是降低并发性和性能。 - 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。 - 如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。 例如下面的代码,`根本起不到锁的作用` ```java public void f() { Object hellis = new Object(); synchronized(hellis) { System.out.println(hellis); } } ``` 代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f( )方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成: ```java public void f() { Object hellis = new Object(); System.out.println(hellis); } ``` 案例: ```java @Fork(1) @BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 3) @Measurement(iterations = 5) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class Demo12 { static int x = 0; @Benchmark public void a() throws Exception { x++; } @Benchmark //JIT 即时编译器 //对热点代码(如循环),超过一定阈值,对代码进行优化 public void b () throws Exception { object o = new object();//o对象是b()的局部变量,没有竞争 //加锁和不加锁都一样,所以实际执行时JIT就把锁消除了 synchronized (o) { x++; } } } ``` `java -jar benchmarks.jar`(打包执行) ```bash Benchmark Mode Samples Score Score error Units c.i. MyBenchmark. a avgt 5 1.542 0.056 ns/op c.i. MyBenchmark. b avgt 5 1.518 0.091 ns/op ``` score值,方法执行时间,越小性能越高,可以看出差不多的 `java -XX:-EliminateLocks -jar benchmarks.jar` 关闭锁消除 ```bash Benchmark Mode Samples Score Score error Units c.i. MyBenchmark. a avgt 5 1.542 0.018 ns/op c.i. MyBenchmark. b avgt 5 16.976 1.572 ns/op ``` 不去做锁消除,可以看出性能差异了。 JVM会做逃逸分析,发现Object o,是局部变量,不会逃出b()方法的作用范围,自然不会被共享,那么锁也不会被其他方法拿到,所以直接消除了锁。 #### 锁粗化 对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。 *附参考原文地址* *《黑马程序员之并发编程》* *https://www.cnblogs.com/wkfvawl/p/15472797.html*
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2501.html
上一篇
15.并发编程之synchronized原理进阶(一)
下一篇
17.并发编程之wait notify
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
查找
Typora
散列
FastDFS
LeetCode刷题
Golang
Stream流
Zookeeper
CentOS
Filter
Flink
并发编程
BurpSuite
Eclipse
DataWarehouse
nginx
Jquery
机器学习
Docker
Spark RDD
MySQL
Spark SQL
递归
二叉树
线程池
Elasticsearch
MyBatisX
Sentinel
哈希表
Scala
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞