友情提示学习更加详细内容查看:多线程锁synchronized挑战_亚索@哈塞给-CSDN博客

4.5 Monitor 概念

Java 对象头

以 32 位虚拟机为例,普通对象的对象头结构如下,其中的klass Word为指针,指向对应的Class对象;

数组对象

其中 Mark Word 结构为

所以一个对象的结构如下:

Monitor 原理

Monitor被翻译为监视器或者说管程

每个java对象都可以关联一个Monitor,如果使用synchronized给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针

  • 刚开始时Monitor中的Owner为null
  • 当Thread-2 执行synchronized(obj){}代码时就会将Monitor的所有者Owner 设置为 Thread-2,上锁成功,Monitor中同一时刻只能有一个Owner
  • 当Thread-2 占据锁时,如果线程Thread-3,Thread-4也来执行synchronized(obj){}代码,就会进入EntryList中变成BLOCKED状态
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

注意:synchronized 必须是进入同一个对象的 monitor 才有上述的效果不加 synchronized 的对象不会关联监视器,不遵从以上规则

synchronized原理

 static final Object lock=new Object();static int counter = 0;public static void main(String[] args) {synchronized (lock) {counter++;}}

反编译后的部分字节码

 0 getstatic #2 <com/concurrent/test/Test17.lock># 取得lock的引用(synchronized开始了)3 dup    # 复制操作数栈栈顶的值放入栈顶,即复制了一份lock的引用4 astore_1# 操作数栈栈顶的值弹出,即将lock的引用存到局部变量表中5 monitorenter# 将lock对象的Mark Word置为指向Monitor指针6 getstatic #3 <com/concurrent/test/Test17.counter>9 iconst_1
10 iadd
11 putstatic #3 <com/concurrent/test/Test17.counter>
14 aload_1
# 从局部变量表中取得lock的引用,放入操作数栈栈顶
15 monitorexit
# 将lock对象的Mark Word重置,唤醒EntryList
16 goto 24 (+8)
# 下面是异常处理指令,可以看到,如果出现异常,也能自动地释放锁
19 astore_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return

注意:方法级别的 synchronized 不会在字节码指令中有所体现

synchronized 原理进阶

轻量级锁

轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是synchronized,假设有两个方法同步块,利用同一个对象加锁

友情提示:轻量级锁,没有线程的竞争,看着就像是串行一样

static final Object obj = new Object();
public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}
}
public static void method2() {synchronized( obj ) {// 同步块 B}
}

1、每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word和对象引用reference

友情提示:每一个线程都会包含锁记录的对象,内部包含对象的引用和储存对象的Mark Word.

2、 让锁记录中的Object reference指向对象,并且尝试用cas(compare and sweep)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录中

友情提示:在线程内部的锁记录中会尝试cas,替换对象的Mark word,将其对象的mark word存储在锁记录中。(就是将其对象中的对象头的mark word存储到线程中的锁记录中)

3、 如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01,如下所示

4、如果cas失败,有两种情况

  1. 如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段
  2. 如果是自己的线程已经执行了synchronized进行加锁,那么再添加一条 Lock Record 作为重入的计数

友情提示:如果cas失败

情况一:存在线程竞争,其他线程持有轻量级锁,将会导致锁升级,进入锁膨胀阶段

情况二:如果是自己线程执行加锁,那么将会再加一条锁记录作为重入的计入(这也体现synchronized是一个可重入锁)

5、当线程退出synchronized代码块的时候,如果获取的是取值为 null 的锁记录 ,表示有重入,这时重置锁记录,表示重入计数减一

6、当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象

  1. 成功则解锁成功
  2. 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。

  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

2、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

  1. 即为对象申请Monitor锁,让Object指向重量级锁地址,然后自己进入Monitor 的EntryList 变成BLOCKED状态

3、当Thread-0 推出synchronized同步块时,使用cas将Mark Word的值恢复给对象头,失败,那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList 中的Thread-1线程

自旋优化(只适合多核CPU)

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁,下面的是介绍两种情况自旋重试成功和自旋重试失败

1、自旋重试成功的情况

2、自旋重试失败的情况,自旋了一定次数还是没有等到持锁的线程释放锁

自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能

偏向锁

在轻量级的锁中,我们可以发现,如果同一个线程对同一个2对象进行重入锁时,也需要执行CAS操作,这是有点耗时,那么java6开始引入了偏向锁,只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS了

偏向状态

友情提示:偏向锁状态默认是延迟的

一个对象的创建过程

  1. 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的Thread,epoch,age都是0,在加锁的时候进行设置这些的值.

  2. 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0来禁用延迟

  3. 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

  4. 实验Test18.java,加上虚拟机参数-XX:BiasedLockingStartupDelay=0进行测试

public static void main(String[] args) throws InterruptedException {Test1 t = new Test1();test.parseObjectHeader(getObjectHeader(t));synchronized (t){test.parseObjectHeader(getObjectHeader(t));}test.parseObjectHeader(getObjectHeader(t));}

输出结果如下,三次输出的状态码都为101

iasedLockFlag (1bit): 1LockFlag (2bit): 01
biasedLockFlag (1bit): 1LockFlag (2bit): 01
biasedLockFlag (1bit): 1LockFlag (2bit): 01

测试禁用:如果没有开启偏向锁,那么对象创建后最后三位的值为001,这时候它的hashcode,age都为0,hashcode是第一次用到hashcode时才赋值的。在上面测试代码运行时在添加 VM 参数-XX:-UseBiasedLocking禁用偏向锁(禁用偏向锁则优先使用轻量级锁),退出synchronized状态变回001

  1. 测试代码Test18.java 虚拟机参数-XX:-UseBiasedLocking

  2. 输出结果如下,最开始状态为001,然后加轻量级锁变成00,最后恢复成001

biasedLockFlag (1bit): 0LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0LockFlag (2bit): 01

撤销偏向锁

方式一:hashcode方法,方式二:其他线程也想得到偏向锁 ,方式二: 调用 wait/notify

撤销偏向锁-hashcode方法

测试 hashCode:当调用对象的hashcode方法的时候就会撤销这个对象的偏向锁,因为使用偏向锁时没有位置存hashcode的值了

  1. 测试代码如下,使用虚拟机参数-XX:BiasedLockingStartupDelay=0 ,确保我们的程序最开始使用了偏向锁!但是结果显示程序还是使用了轻量级锁。 Test20.java

public static void main(String[] args) throws InterruptedException {Test1 t = new Test1();t.hashCode();test.parseObjectHeader(getObjectHeader(t));synchronized (t){test.parseObjectHeader(getObjectHeader(t));}test.parseObjectHeader(getObjectHeader(t));}

输出结果

biasedLockFlag (1bit): 0LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0LockFlag (2bit): 01

撤销偏向锁-其它线程使用对象

这里我们演示的是偏向锁撤销变成轻量级锁的过程,那么就得满足轻量级锁的使用条件,就是没有线程对同一个对象进行锁竞争,我们使用waitnotify 来辅助实现

  1. 代码 Test19.java,虚拟机参数-XX:BiasedLockingStartupDelay=0确保我们的程序最开始使用了偏向锁!

  2. 输出结果,最开始使用的是偏向锁,但是第二个线程尝试获取对象锁时,发现本来对象偏向的是线程一,那么偏向锁就会失效,加的就是轻量级锁

友情提示:偏向锁失效转换成轻量级锁

biasedLockFlag (1bit): 1LockFlag (2bit): 01
biasedLockFlag (1bit): 1LockFlag (2bit): 01
biasedLockFlag (1bit): 1LockFlag (2bit): 01
biasedLockFlag (1bit): 1LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0LockFlag (2bit): 01

撤销 - 调用 wait/notify

会使对象的锁变成重量级锁,因为wait/notify方法之后重量级锁才支持

批量重偏向

如果对象被多个线程访问,但是没有竞争,这时候偏向了线程一的对象又有机会重新偏向线程二,即可以不用升级为轻量级锁,可这和我们之前做的实验矛盾了呀,其实要实现重新偏向是要有条件的:就是超过20对象对同一个线程如线程一撤销偏向时,那么第20个及以后的对象才可以将撤销对线程一的偏向这个动作变为将第20个及以后的对象偏向线程二。

友情提示:就是重新偏向到另一个线程。临界值是20,就是对应线程一撤销偏向锁20次,就会转向其他线程获得偏向锁

批量撤销

锁消除和锁粗化

锁粗化

  锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。


public class StringBufferTest {StringBuffer stringBuffer = new StringBuffer();public void append(){stringBuffer.append("a");stringBuffer.append("b");stringBuffer.append("c");}
}

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

友情提示:锁粗化说白了,就是将多个较小范围的锁,变成一个大范围的锁。

消除锁

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,所以其实这过程是线程安全的,JVM会自动将其锁消除。

/*** @author lihongxu* @desc 消除StringBuffer同步锁* @since 2019/3/20 上午9:37*/
public class StringBufferRemoveSync {public void add(String str1, String str2) {//StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用//因此sb属于不可能共享的资源,JVM会自动消除内部的锁StringBuffer sb = new StringBuffer();sb.append(str1).append(str2);}public static void main(String[] args) {StringBufferRemoveSync rmsync = new StringBufferRemoveSync();for (int i = 0; i < 10000000; i++) {rmsync.add("abc", "123");}}
}

友情提示:说白了,就是将其锁去掉。

JUC之synchronized原理阶段三相关推荐

  1. 【重难点】【JUC 04】synchronized 原理、ReentrantLock 原理、synchronized 和 Lock 的对比、CAS 无锁原理

    [重难点][JUC 04]synchronized 原理.ReentrantLock 原理.synchronized 和 Lock 的对比.CAS 无锁原理 文章目录 [重难点][JUC 04]syn ...

  2. 锁,CAS,Synchronized 原理

    作者:~小明学编程 文章专栏:JavaEE 格言:热爱编程的,终将被编程所厚爱. 目录 常见的锁 悲观锁与乐观锁 悲观锁 乐观锁 读写锁 重量级锁 vs 轻量级锁 挂起等待锁和自旋锁 公平锁和非公平锁 ...

  3. 打工人,从 JMM 透析 volatile 与 synchronized 原理

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 在面试.并发编程.一些开源框架中总是会遇到 volatil ...

  4. 从 JMM 透析 volatile 与 synchronized 原理

    作者 | 李健青 来源 | 码哥字节(ID:MageByte) 头图 |  CSDN 下载自东方IC 在面试.并发编程.一些开源框架中总是会遇到 volatile 与 synchronized .sy ...

  5. 锁策略、CAS、synchronized原理

    1.常见的锁策略 (1)乐观锁 和 悲观锁 乐观锁:预测锁竞争的情况不激烈(工作量较少) 悲观锁:预测锁竞争的情况很激烈(工作量较多) (2)轻量级锁 和 重量级锁 轻量级锁:加锁和解锁的开销较小,效 ...

  6. StringBuffer、StringBuilder区别以及Synchronized原理

    1.为什么StringBuffer是线程安全的StringBuilder是线程不安全的 这里我只是列举了几个方法来对比,其他方法对比可以查看两个类的源码,从上面的截图可以看到StringBuffer实 ...

  7. 并发编程之Synchronized原理

    一.基本使用    1.Synchronized的作用. 原子性:确保线程互斥的访问同步代码: 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 "对一个变量unloc ...

  8. synchronized原理_synchronized关键字的作用、原理以及锁优化

    synchronized简介 synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以隐式地扮演一个用于同步的锁的角色.这些Java内置的使用者看不到的锁被称为内部锁(i ...

  9. Java 并发编程CAS、volatile、synchronized原理详解

    CAS(CompareAndSwap) 什么是CAS? 在Java中调用的是Unsafe的如下方法来CAS修改对象int属性的值(借助C来调用CPU底层指令实现的): /*** * @param o ...

最新文章

  1. python selenium 自动登录_windows7 python3.63使用selenium+webdriver 实现自动登录使用过程...
  2. php 经纬度 距离排序,php mysql 根据经纬度计算距离和排序
  3. (*长期更新)软考网络工程师学习笔记——Section 12 Linux系统与文件管理命令
  4. 开源作者在行动:疫情防控相关开源项目推荐
  5. 深入理解JavaScript系列(13):This? Yes,this!
  6. 《JavaScript高级程序设计(第3版)》.Nicholas.C.Zakas.扫描版.pdf
  7. 手机APP测试需要注意的问题
  8. python 自动登录网站_Python使用selenium实现网页用户名 密码 验证码自动登录功能...
  9. apple登录服务端验证
  10. strapi终于装好了,网速太慢了,处理了一下代理,新建了一个.zshrc文件,加入了pon和poff两个函数
  11. 音频线视频线和同轴电缆的关系(同轴线除了外面的屏蔽网还有中间的绝缘塑料体,而音频线一般只有外面的屏蔽网)
  12. 2021 ATTCK v10版本更新指南
  13. 【图像融合】基于matlab主成分结合小波离散变换PCA-DWT图像融合【含Matlab源码 2199期】
  14. 新装的windows遇到命令行脚本无法执行
  15. 如何使用群晖nas快速收集多份文件?
  16. Winlogon通知包(Winlogon Notification Package)
  17. idea注释不顶格(不在行首)
  18. php asoft 排序,卓象科技:PHP算法之归并排序
  19. 探秘亿联网络最新声学实验室
  20. JavaScript程序基础(六)循环语句

热门文章

  1. Apifox如何实现私有化部署?解决方案
  2. webpack-chain项目中文翻译
  3. html jq页面跳转页面,jQuery实现页码跳转式动态数据分页
  4. 上交2017计算机专业就业,2017年大学专业就业率排行榜 看看有没有你喜欢的专业...
  5. 移动办公市场叫好不叫座 CRM尚需努力
  6. css的font-family的中英文对照
  7. python求向量函数的雅可比矩阵_使用python,pytorch求海森Hessian矩阵
  8. 【18.5.31 日常】Android项目——飞机大战详解
  9. 【5G入门】MCS调制编码方案简介
  10. 怎样挑选适合学生用的台灯?盘点学生护眼灯品牌排行榜