Synchronized_源码级深度分析

一、应用总结

synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作 一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。
在这里插入图片描述

二、字节码解析

synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语 Mutex(互斥量),它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的 优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操 作的开销,内置锁的并发性能已经基本与Lock持平。

同步方法 是通过方法上的 access_flags中设置ACC_SYNCHRONIZED标志来实现;
在这里插入图片描述
在这里插入图片描述

同步代码块是通过monitorentermonitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥 原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态 之间来回切换,从而导致性能损耗,所以说synchronized是一个重量级锁。
在这里插入图片描述

三、Monitor(管程/监视器)

monitor 在操作系统层面的专业术语叫“管程”,管程指的是管理共享变量和对共享变量的操作过程,让它们支持并发;

3.1、MESA模型

mesa是属于解决管程相关的一种模型,也是被广泛使用最多的一种;
在这里插入图片描述

3.2 monitor的主要数据结构

hotspot ObjectMonitor.hpp 文件中

 //ObjectMonitor.hpp
 // initialize the monitor, exception the semaphore, all other fields
  // are simple integers or pointers
  ObjectMonitor() {
    _header       = NULL; //对象头
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;    //锁的重入次数
    _object       = NULL;
    _owner        = NULL; //标识拥有该monitor的线程(当前获取锁的线程)
    _WaitSet      = NULL; //等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失 败的线程)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

四、对象布局

在这里插入图片描述
Mark Word是如何记录锁状态的
Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//
enum { locked_value             = 0,  //00轻量级所
         unlocked_value           = 1,  // 001 无锁
         monitor_value            = 2,  //10 重量级锁
         marked_value             = 3, //11 GC标记
         biased_lock_pattern      = 5  //101 偏向锁
  };
// Constants
  enum { age_bits                 = 4,
         lock_bits                = 2,
         biased_lock_bits         = 1,
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2
  };

在这里插入图片描述

五、JOL工具验证

5.1 包依赖

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.11</version>
</dependency>

5.2 场景验证

  • 偏向锁
    偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 延迟才会对每个新创建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等操作。这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。 为了减少初始化时间,JVM默认延时加载偏向锁。
‐XX:BiasedLockingStartupDelay=0 //设置偏向锁启动延迟,默认值是4000ms
‐XX:UseBiasedLocking //禁止偏向锁
‐XX:+UseBiasedLocking //开启偏向锁

代码验证

public class BiasedLockDemo {
    private static final Logger log= LoggerFactory.getLogger(BiasedLockDemo.class);
    public static void main(String[] args) throws InterruptedException {
        log.debug("创建的对象========"+ClassLayout.parseInstance(new Object()).toPrintable());
        Thread.sleep(4000);
        log.debug("4秒后创建的对象======="+ClassLayout.parseInstance(new Object()).toPrintable());
    }
}

在这里插入图片描述

public class BiasedLockDemo {

    private static final Logger log= LoggerFactory.getLogger(BiasedLockDemo.class);

    public static void main(String[] args) throws InterruptedException {
        /*log.debug("创建的对象========"+ClassLayout.parseInstance(new Object()).toPrintable());
        Thread.sleep(4000);
        log.debug("5秒后创建的对象======="+ClassLayout.parseInstance(new Object()).toPrintable());
*/
        Thread.sleep(4000);
        Object obj = new Object();
        new Thread(()->{
            log.debug(Thread.currentThread().getName()+"  开始执行  "+ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj){
                log.debug(Thread.currentThread().getName()+"  获取锁执行  "+ClassLayout.parseInstance(obj).toPrintable());
            }
            log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
        },"thread-1").start();

        Thread.sleep(4000);

        log.debug("最终======= "+ClassLayout.parseInstance(obj).toPrintable());
    }
}

在这里插入图片描述
偏向锁撤销之调用对象HashCode
调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存 hashcode的。
轻量级锁会在锁记录中记录 hashCode
重量级锁会在 Monitor 中记录 hashCode

当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也 无法偏向:
当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。

public class BiasedLockDemo {

    private static final Logger log= LoggerFactory.getLogger(BiasedLockDemo.class);
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(4000);
        Object obj = new Object();
        log.debug(Thread.currentThread().getName()+" 没用调用HashCode== "+ClassLayout.parseInstance(obj).toPrintable());
        obj.hashCode();
        new Thread(()->{
            log.debug(Thread.currentThread().getName()+"  开始执行  "+ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj){
                log.debug(Thread.currentThread().getName()+"  获取锁执行  "+ClassLayout.parseInstance(obj).toPrintable());
            }
            log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
        },"thread-1").start();

        Thread.sleep(4000);
        log.debug("最终======= "+ClassLayout.parseInstance(obj).toPrintable());
    }
}

在这里插入图片描述

        Thread.sleep(4000);
        Object obj = new Object();
        new Thread(()->{
            log.debug(Thread.currentThread().getName()+"  开始执行  "+ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj){
                log.debug(Thread.currentThread().getName()+"  加锁成功 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
                obj.hashCode();
                log.debug(Thread.currentThread().getName()+"  加锁成功 调用HashCode "+ClassLayout.parseInstance(obj).toPrintable());
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
        },"thread-1").start();

在这里插入图片描述
偏向锁撤销之调用wait/notify
偏向锁状态执行obj.notify() 会升级为轻量级锁,调用obj.wait(timeout) 会升级为重量级锁

        Thread.sleep(4000);
        Object obj = new Object();
        new Thread(()->{
            log.debug(Thread.currentThread().getName()+"  开始执行  "+ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj){
                log.debug(Thread.currentThread().getName()+"  加锁成功 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
                obj.notify();
                log.debug(Thread.currentThread().getName()+"  加锁成功 调用notify "+ClassLayout.parseInstance(obj).toPrintable());
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
        },"thread-1").start();

在这里插入图片描述

        Thread.sleep(4000);
        Object obj = new Object();
        new Thread(()->{
            log.debug(Thread.currentThread().getName()+"  开始执行  "+ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj){
                log.debug(Thread.currentThread().getName()+"  加锁成功 开始执行 "+ClassLayout.parseInstance(obj).toPrintable());
                try {
                    obj.wait(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug(Thread.currentThread().getName()+"  加锁成功 调用wait "+ClassLayout.parseInstance(obj).toPrintable());
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug(Thread.currentThread().getName()+" 释放锁 "+ClassLayout.parseInstance(obj).toPrintable());
        },"thread-1").start();

在这里插入图片描述

  • 轻量级锁
    如果偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁结构。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。

偏向锁升级轻量级锁

public static void main(String[] args) throws InterruptedException {
        Thread.sleep(4000);
        Object obj = new Object();
        Thread thread1 = new Thread(() -> {
            log.debug(Thread.currentThread().getName() + "  开始执行  " + ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj) {
                log.debug(Thread.currentThread().getName() + "  加锁成功执行中 " + ClassLayout.parseInstance(obj).toPrintable());
            }
            log.debug(Thread.currentThread().getName() + " 释放锁 " + ClassLayout.parseInstance(obj).toPrintable());
        }, "thread-1");
        thread1.start();

        //设置竞争不那么激励
        Thread.sleep(200);

        Thread thread2 = new Thread(() -> {
            log.debug(Thread.currentThread().getName() + "  开始执行  " + ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj) {
                log.debug(Thread.currentThread().getName() + "  加锁成功执行中 " + ClassLayout.parseInstance(obj).toPrintable());
            }
            log.debug(Thread.currentThread().getName() + " 释放锁 " + ClassLayout.parseInstance(obj).toPrintable());
        }, "thread-2");
        thread2.start();

        Thread.sleep(4000);
        log.debug("最终======= "+ClassLayout.parseInstance(obj).toPrintable());
    }

在这里插入图片描述
1.

六、锁升级总结

在这里插入图片描述

七、锁优化

  1. 偏向锁批量重偏向&批量撤销

从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。
应用场景
批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操 作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。 批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的
原理
以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销 操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问 题,因此会进行批量重偏向。 每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该 字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同 时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新 值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其 他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线 程Id。当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后 (默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后, 对于该class的锁,直接走轻量级锁的逻辑。
JVM参数

intx BiasedLockingBulkRebiasThreshold = 20 //默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 //默认偏向锁批量撤销阈值

总结
1. 批量重偏向和批量撤销是针对类的优化,和对象无关。
2. 偏向锁重偏向一次之后不可再次重偏向。
3. 当某个类已触发批量撤销机制后,JVM会默认当前类产生了严重问题,剥夺该类的新实例对象使用偏向锁权利

  1. 自旋锁优化
    重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程 已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
    自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调 用,存在用户态和内核态切换,这才是重量级锁最大的开销)

  2. 锁粗化

  StringBuffer buffer = new StringBuffer()
  buffer.append("aaa").append(" bbb").append(" ccc");
  1. 锁消除
    使用逃逸分析,编译器可以对代码做如下优化:
    1.同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问 到,那么对于这个对象的操作可以不考虑同步。
    2.将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象 的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
    3.分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存 在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

八、源码分析

轻量级锁源码分析流程图
重量级锁加锁解锁源码分析流程图

其他
synchronized初识

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值