你真的了解Synchronized吗?深入JVM,图文并茂的理解Synchronized的加锁原理(锁的概念、同步关键字的概念、同步关键字的原理)

提纲

你真的了解Synchronized吗?深入JVM,图文并茂的理解Synchronized的加锁原理(锁的概念、同步关键字的概念、同步关键字的原理)_第1张图片


认识锁

锁的概念

悲观锁

  • 概念:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
  • 实例:Synchronized 就是悲观锁

乐观锁

  • 概念:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。
  • 实例:CAS机制

自旋锁

  • 概念:不放弃CPU事件,不断使用 CAS 尝试对数据进行更新,直到成功
  • 实例:AtomicInt 使用自旋锁,保证数据的原子性
  • 其实也是一种乐观锁

可重入锁

  • 概念:同一个线程,在拿到一次锁之后,可以继续调用同一把锁(owner)进行同步的代码。
  • 实例:synchronized 就是可重入锁

独享锁

  • 概念:给资源加上写锁,线程可以修改资源,其他线程不能再加锁
  • 实例:ReentrantReadWritelock 的 WriteLock

共享锁

  • 概念:给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁; (多读)
  • 实例:ReentrantReadWritelock 的 ReadLock

公平锁、非公平锁

  • 概念:对资源的争抢是否有严格的先后顺序。在抢锁时,先尝试抢一下,再把自己加入等待队列。

 

synchronized

概念

  • 最基本的线程通信机制,是基于监视器对象(Monitor)实现的
  • 每个对象都会关联一个线程监视器,线程可以对他加锁/解锁
  • 一次只能有一个线程锁定监视器,当监视器被锁定,其他线程获取时会被阻塞。
  • 很重要:同步关键字保证了内存可见性

特性

  • 可重入:当一个线程获取监视器后,可以多次调用临界区diamante
  • 独享:一次只能有一个线程锁定监视器
  • 悲观锁:直接获取锁

范围

  • 类、对象

 

锁消除/锁粗化

  • 属于运行时的 JIT 编译优化
    • 拓展:通过JitWatch查看经过JIT优化后的代码

锁消除

  • 对于某些局部变量的代码,可能不会出现线程安全问题,那样锁就会被消除
public void genStr(){
    //JIT 优化,消除了锁
    StringBuffer sb = new StringBuffer();
    sb.append("a");
    sb.append("b");
    sb.append("c");
}
  • 优化前:出现了 Lock 相关的汇编指令(从左到右依次是:代码/Java指令/汇编指令)

  • 优化后:没有了锁的存在

锁粗化

  • JIT基于性能考虑,可能会将锁的范围扩大
public class LockDemo01{
    int count;
    public void runTest(){
         for(int i = 0; i < 10000; i++){
            //sych 会锁到 for 循环外面
            synchronized(this){
                count++;   
            }
         }
    }
}

 

Synchronized加锁原理

MarkWord    

一个对象有三部分,MarkWord 就是其中的一部分。可以理解为对象头的一部分。

同步关键字,其实就是操作对象的内存

你真的了解Synchronized吗?深入JVM,图文并茂的理解Synchronized的加锁原理(锁的概念、同步关键字的概念、同步关键字的原理)_第2张图片

  • HotSpot中,对象前面会有一个类指针和标题,储标识哈希码的标题字以及用于分代垃圾收集的年龄和标记位
  • 默认情况下JVM锁会经历:偏向锁->轻量级锁->重量级锁这四个状态。
    • 偏向锁:TheadId
    • 轻量级锁:Lock Record Address,存储线程栈中 LockRecord 的地址
    • 重量级锁:Monitor Address,存储 monitor obj 的地址
      • 因为涉及到了 Monitor 对象,所以可以认为是重量级的
  • 相关文档:HotspotOverview.pdf

 

锁状态的改变

无锁->轻量级锁

 

  • 概念
    • LockRecords:线程持有锁的信息,一个线程可能会持有多把锁,LockRecords 记录了当前拿到了哪些对象的锁
    • MarkWord:记录了当前锁定的线程栈的地址
  • 过程
    • 加锁:使用CAS修改 MarkWord 完毕,加锁成功。则 MarkWord 中的 tag 进入00状态。
    • 解锁:则是一个逆向恢复mark word的过程
  • 总结
    • 解锁和解锁,其实就是一个 CAS 修改内存(MarkWord/线程栈)的过程

 

偏向锁 -> 轻量级锁

你真的了解Synchronized吗?深入JVM,图文并茂的理解Synchronized的加锁原理(锁的概念、同步关键字的概念、同步关键字的原理)_第3张图片

  • 概念
    • 偏向标记:在出现争用后就失效了(未出现争用则不会失效)
      • -XX: -UseBiasedLocking 禁用使用偏置锁定
    • 偏向锁:本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步
      • 通过 ThreadId 去判断是否为当前线程,只是判断,不会进行 CAS 操作
  • 问题1:为什么有偏向锁
    • 同步在 JVM 底层是需要很多操作来实现,如果没有出现争用,就不需要进行同步操作
  • 问题2:线程重入是如何判定的
    • 因为在 MarkWord 中记录了当前持有锁的线程的 ThreadID,所以可以比对出,是否可以重入

 

轻量级锁 -> 重量级锁

你真的了解Synchronized吗?深入JVM,图文并茂的理解Synchronized的加锁原理(锁的概念、同步关键字的概念、同步关键字的原理)_第4张图片

  • 修改 MarkWord 如果失败,会自旋 CAS 一定次数(可以通过参数配置)
  • 超过次数,仍未抢到锁,则锁升级为重量级锁,进入阻塞。
  • Monitor 也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的 Monitor。

 

加锁流程总结

重量级锁/轻量级锁/未锁定

相关代码:src.share.vm.runtime.ObjectMonitor

你真的了解Synchronized吗?深入JVM,图文并茂的理解Synchronized的加锁原理(锁的概念、同步关键字的概念、同步关键字的原理)_第5张图片

  1. 当 线程-1 请求持有锁时,如果对象时 01 (unlock),则会获得锁,并将对象头中的标志位置为 轻量级锁 (00)
    1. 线程是通过 CAS 自旋的方式去请求锁的
  2. 当对象状态位 00 时,有其他线程请求锁,线程首先通过 CAS 自旋的方式去尝试获得锁,当尝试达到次数没有获得时,对象的状态会变为 10.
    1. 这里不再使用自旋等待的原因是自旋会消耗大量资源
    2. 之所以是重量级,是因为要操作两个对象 > 原对象、monitor 对象
  3. 此时对象的 monitor 中,_owner 会变为 线程-1,新的请求线程会放到 _EntryList 中等待
    1. _EntryList 是争抢队列!!!
  4. 线程释放 _owner 会有两种方式
    1. 当持有锁的线程执行完后,会 monitorExit.
    2. 调用对象的 wait 方法后,会进入等待集合 _waiters
    3. 线程要进入 _waiters 队列,需要 _owner 线程调用 wait 方法,所以需要在同步代码块中执行 wait()/notify() 操作
    4. 因为等待集合是 Set,所以 notify 唤醒的时候不确定唤醒哪个

 

偏向锁

优先级: 未锁定 >>> 偏向锁 >>> 轻量级 >>> 重量级

你真的了解Synchronized吗?深入JVM,图文并茂的理解Synchronized的加锁原理(锁的概念、同步关键字的概念、同步关键字的原理)_第6张图片

  • 轻量级锁的标志位存在 对象头 的 MarkWord 中,默认是开启的
  • 升级过程
    • 线程1 请求持有锁,会先检查偏向锁锁的标志位,如果为 1 打开,则通过 CAS 修改线程信息。
      • 线程信息中记录了当前获得偏向锁的线程的 ThreadId
    • 如果修改成功,就拿到了锁
    • 如果通过 CAS 修改失败,且线程状态是 01 未锁定,则升级到轻量级锁被清空标志位。(产生了锁竞争)
      • 如果此时已锁定,则要进行 CAS 争抢
  • 偏向锁释放,会清空线程信息
  • 为什么要用偏向锁
    • 因为 JVM 的设计理念认为,大多数情况下,并不存在锁竞争,不需要频繁修改标志位,减少了变为 重量级锁 的可能性。带来了性能提升。

 

拓展:通过JitWatch查看经过JIT优化后的代码

  • 输出jit日志
    • windows:在 jre/bin/server 放置 hsdis 动态链接库
    • eclipse、idea等工具,加上JVM参数
-server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=jit.log
  • 工具安装
    • 下载  https://github.com/AdoptOpenJDK/jitwatch
    • 解压 通过maven运行
    • mvn clean compile exec:java
  • 配置jitwatch
    • 页面选择 config, 配置要调试的项目src源码路径,和class编译路径
    • 打开jit.log
    • 点击start
  • 在分析的结果中,选中指定的类,再选择右侧的具体方法,则弹出jit编译结果

你可能感兴趣的:(并发编程)