Java-第十六部分-JUC-synchronized、锁和对象头

312 阅读22分钟

JUC全文

synchronized

  • 来修饰方法、代码块。属于独占锁、悲观锁、可重入锁、非公平锁、互斥锁、同步锁
  • 保证代码线程安全,原子运行

锁方法

  • 锁普通方法
synchronized void add() {
    num++;
}
//等价于
void add() {
    synchronized (this) {
        num++;
    }
}
  • 锁静态方法
synchronized static void add() {
    num++;
}

//等价于
static void add() {
    synchronized (Test.class) {
        num++;
    }
}
  • 优化,用原子类
AtomicInteger integer = new AtomicInteger();
public AtomicInteger getInteger() {
    return integer;
}
void add() {
    //优化
    integer.incrementAndGet();
}

底层

  • 每个对象都有个monitor对象,加锁就是在竞争monitor对象,当monitor被占用时,就会处于锁定状态,线程执行monitorenter尝试获取monitor
  • wait/notify基于monitor对象,静态方法中不能使用,且只能锁对象去调用,wait后的线程被notify唤醒后,会继续尝试抢锁,执行下面的代码

monitor对象

  • header记录锁对象的markword
  • count记录个数,被获取时++,被释放时--
  • Contention List / cxq所有请求锁的线程将被首先放置到该竞争队列,单向链表
  • waitSet 处理wait状态的线程,会被加入到waitSet,等待唤醒,循环双向链表
  • EntryList处理等待锁block状态的线程,会被加入该队列,双向链表
  • Owner当一个线程获取到这个monitor对象,会指向这个线程,当线程释放掉之后,owner置为null
  • 当多个线程同时访问一段同步代码时
  1. 每个等待锁的线程都会被封装成ObjectWaiter,插入cxq队列的队首,调用park,使线程挂起,进入blocked状态,后续根据唤醒策略不同,决定是否放入entryList,取cxq或者EntryList
  2. 当线程获取到对象的monitor后,并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1
  3. 若线程调用wait()方法,将释放当前持有的monitorowner变量恢复为nullcount自减1,同时该线程进入WaitSet集合中等待被唤醒,唤醒后,移动到cxqentryList继续争锁
  4. 若当前线程执行完毕,将owner置为null,也将释放monitor并复位count的值,以便其他线程进入获取monitor
  • 唤醒策略,当释放锁会调用
  1. QMode == 2 且cxq非空,取队首对象,并唤醒该线程,并直接返回
  2. QMode == 3 且cxq非空,把cxq队列插到EntryList末尾
  3. QMode == 4 且cxq非空,把cxq队列插到EntryList队首
  4. QMode == 0 什么都不做
  • 后续
  1. 如果EntryList队首非空,唤醒该线程
  2. 如果EntryList为空,将cxq放入EntryList,如果QMode == 1 则需要反转顺序,否则直接加入,并取队首元素
  • notify策略,此时并未真正唤醒等待线程,而是将等待的线程加入队列,当锁对象进行exit后,真正从cxq/EntryList取下一个线程
  1. 放入EntryList队首
  2. 放入EntryList末尾
  3. EntryList为空就放入EntryList,否则放入cxq队首
  4. 放入cxq末尾
  • 以上也可以看出,synchronized的非公平特点,后来的等待的反而可能先获得锁

字节码层面

  • 当修饰代码块时,是在代码块前后分别加上monitorentermonitorexit指令来实现的

有两个monitorexit,第二个为异常退出 image.png

  • 当直接修饰方式时,字节码层面没有指令,会在方法访问标识上体现

执行线程将先获取monitor(klass类型的),获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象 image.png

  • monitorenter
  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1,重入机制
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
  • monitorexit
  1. 执行monitorexit的线程必须是锁对象所对应的monitor的所有者
  2. monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者
  3. 其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权

细节

  • synchronized 1.6之前,为重量级锁,没有拿到锁的线程,会被加入到队列中 image.png
  • synchronized 1.6之后,锁升级 image.png

和Lock的区别

  • Lock
  1. 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁
  2. 需要手动获取锁和释放锁
  3. 发生异常时,如果没有主动通过unlock()释放锁,会发生死锁现象,使用时需要在finally中释放
  4. 可以让等待的线程响应中断
  5. 可以通过lock.isLocked()知道有没有成功获得锁
  6. 可以通过读写锁提高多个线程读操作的效率
  • synchronized
  1. 关键字,内置的语言实现
  2. 在发生异常时,自动释放线程锁,不会导致死锁
  3. 等待的线程会一直等待下去,不能响应中断
  4. 无法知道是否获取锁
  • synchronized优势
  1. 简单
  2. 异常时,自动释放锁,抛出异常
  3. 使用Lock时,很难知道哪些锁对象被线程持有,而synchronized需要程序员写入锁对象

和ReentrantLock的区别

image.png

  • ReentrantLock,继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁
  • 相同点
  1. 解决了共享变量安全访问的问题
  2. 都是可重入锁,递归锁,同一个线程可以多次获得一个锁
  3. 保证了线程安全两大特性,可见性/原子性
  • 不同点
  1. ReentrantLock手动显式操作锁;synchronized隐式操作锁
  2. ReentrantLock可响应中断;synchronized不可响应中断
  3. ReentrantLockAPI级别的;synchronizedJVM级别的
  4. ReentrantLock可以实现公平锁/非公平锁;synchronized只是非公平锁
  5. ReentrantLock可以通过Condition绑定多个条件,通过await()/signal(),在特定的情况下休眠/唤醒,达到线程间通信
  6. ReentrantLock对于等待的线程,先来先获得;synchronized对于等待的线程,可能后来先获得

锁类别

  • 锁膨胀/升级,线程竞争;不得不(偏向锁还未初始化完成,只能由无锁膨胀成轻量级锁)

无锁

  • 对于一个对象来说,是该对象没有成为锁对象
  • 对于CAS来说是,代码层面没有锁,但是实际上在汇编级别和硬件级别会加锁

匿名偏向锁与偏向锁

  • 需要模拟出匿名偏向锁,需要延迟偏向锁初始化4秒以上
Thread.sleep(5000);
Test test = new Test();
System.out.println(ClassLayout.parseInstance(test).toPrintable());
//test.hashCode();
synchronized (test) {
    System.out.println(ClassLayout.parseInstance(test).toPrintable());
}
  • 匿名偏向锁与偏向锁 image.png
  • 加上test.hashCode();,变成了轻量级锁,先会撤销为无锁,再变为轻量级锁,需要记录对象的hashcode image.png

匿名偏向锁

  • 锁标识位是101,锁对象头的线程id记录为null
  • 当被当作同步执行的锁对象时,膨胀成偏向锁

偏向锁

  • 适用于单个线程重入的场景,在无竞争的情况下把整个同步都消除掉,不去做CAS操作 image.png
  • 偏向某个线程,优化大部分时间只有一个线程执行的同步代码,如果在接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程将不需要进行同步操作

记录线程id,放锁对象内部暂存,当有线程要执行这块代码时,只需要判断线程id是否相等,不需要额外的判断,直接执行

  • 优点,把整个同步都消除,CAS操作都不去做,优于轻量级锁
  • 缺点,如果程序中大多数的锁总是被多个不同线程访问,那偏向锁就是多余的
  • 偏向锁锁撤销,当其他线程进行竞争时发生
  1. 需要等到全局安全点(这个时间点上没有字节码正在执行),栈帧是动态的,要遍历栈帧中的lock record
  2. 暂停拥有偏向锁的线程,检查持有偏向锁的线程是否存活,如果线程不处于活动状态,设置为无锁或者匿名偏向
  3. 如果线程活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录BasicObjecLock,如果此时还在同步方法内,需要升级为轻量级锁,原线程仍持有锁;如果不在同步方法内,撤销为无锁或者匿名偏向锁
  • 如果将线程id写入当前锁对象头,则代表抢锁成功
  • 解锁,将最近一条lock record/BasicObjecLock设置为null

轻量级锁与重量级锁

  • 重量级锁,涉及线程阻塞、上下文切换、操作系统线程调度、内存态/用户态切换,比较占系统资源
  • 轻量级锁,比较不占系统资源,线程不阻塞、较少的切换、较少的线程调度
  • 轻量级锁不一定比重量级锁效率高

当大量线程CAS空转、自旋不成功,会大量消耗cpu资源;此时,不如直接放入队列,使用重量级锁 image.png

轻量级锁

  • 适用于两个线程交替执行,不发生冲突的场景
  • 无竞争的情况下,使用CAS操作消除同步使用的互斥量,在没有多线程竞争的前提下,减少重量级锁使用操作系统互斥量产生的性能消耗,如果两条以上线程争用同一个锁,轻量级锁将不会有效,必然膨胀成重量级锁
  1. 优先,如果没有竞争,通过CAS操作避免互斥量的开销,减少线程切换
  2. 缺点,如果存在竞争,不仅需要转换成重量级锁,产生互斥量的开销,还有额外的CAS操作的开销
  • 在虚拟机内部,使用一个BasicObjecLock的对象实现,这个对象内部由一个BasicLock对象(BasicLock_lock)一个持有该锁的Java对象指针(oop_obj)组成。 image.png
  • 实现过程
  1. 首先,BasicLock通过set_displaced_header()方法备份了原对象的Mark Word,并且是无锁状态,为了恢复成无锁状态做准备。
  2. BasicLock还会作为重入标识,如果不是重入,存储无锁状态的Mark Word;如果是重入,则为null;对于双层sync,里面一层的重入标识为null,跳出一层后,判断为null,不回写,最外层不为null,则需要回写,进行锁撤销
  3. 每次重入,一个锁对象都会在线程栈中生成一个BasicLock,并依次弹出
  4. 接着,使用CAS操作,尝试将BasicLock的地址复制到对象头的Mark Word。如果复制成功,那么加锁成功。如果加锁失败,那么轻量级锁就有可能被膨胀为重量级锁
  • BasicobjectLock对象放置在Java栈的栈帧中的栈顶,局部变量表的上面
  • 解锁时,将BasicLock中的值还给该对象,如果替换成功,则解锁完成;如果失败,则说明有其他线程在等待这个锁,那么锁升级为重量锁,需要去monitorEntryList队列,唤醒一个线程

重量级锁

  • c++中的类ObjectMonitorMark Word指向monitor的地址
  • 底层借助内核态的mutex结构体互斥量,涉及到应用态/用户态和内核态的切换(中断触发) image.png
  • synchorized通过对象内部的一个监视器锁 monitor来实现,依赖底层操作系统饿的mutex lock实现
  • 操作系统实现线程的切换需要从用户态切换到和心态,成本非常高

公平锁和非公平锁

  • synchroized是非公平锁,ReentrantLock通过构造函数指定,默认非公平,true公平,false非公平
  • 是一种思想
  • 唯一的差异是存在一个临界区,占有锁的线程已经执行完任务,释放了锁,将状态位恢复,这时恰好来了一个线程的时候
  1. 公平锁,多个线程按照申请锁的顺序来获取锁,通过队列(AQS其实是双向链表)排队,要求先来后到;如果当前等待队列为空,则占有锁,如果等待队列不为空,则加入到等待队列的末尾
  2. 非公平锁,新来的线程直接尝试获取锁,如果抢不到,则按照公平锁的方式排队,在排队的线程依次获取
  • 公平锁 image.png
  • 非公平锁
  1. 优点,非公平锁的性能高于公平锁
  2. 缺点,有可能造成线程饥饿,某一个线程长时间获取不到锁 image.png

乐观锁与悲观锁

  • 乐观锁,乐观思想,可以同时进行读操作,读的时候不能进行写操作;只允许一个线程进行写操作,写的时候不能读
  1. 面对读多写少的场景,遇到并发写的概率较低
  2. 认为读的同时不会进行修改,不会上锁
  3. 写数据时,通过CAS方式,进行写
  4. java中的乐观锁,CAS、RenntrantReadWriteLock
  • 悲观锁,悲观思想,只能有一个线程进行读操作或者写操作,其他线程的读写操作均不能进行
  1. 面对写多读少的场景,遇到并发写的可能高
  2. 每次读数据都会被认为被其他线程修改,每次读写数据都会上锁
  3. 其他线程想要读写这个数据,就会被锁持有线程block,直到释放
  4. java中的悲观锁,synchronizedReentrantLock

自旋锁

  • CAS操作中的比较操作失败后的自旋等待
  • 是一种技术,为了让线程等待,让线程进行一个忙循环,不放弃处理器的执行时间,不断检查持有锁的线程是否会释放锁
  • 优点,避免线程切换开销,挂起线程和恢复线程的操作都需要转入内核态完成,会给应用带来性能压力
  • 缺点,占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,带来性能的浪费

自旋等待的时间需要有一定限度,如果自旋超过限定次数仍然没有成功获得锁,就使用传统方式挂起(转换为重量级锁,加入队列等待)

  • 自旋次数的默认值,10次,可以使用-XX:PreBlockSpin更改

jdk6/7之后失效,每个monitor上记录自适应自旋的信息,来决定在阻塞前要尝试自旋多少次,使用动态调整的次数

  • 自适应自旋,意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状态预测就会越来越精准

可重入锁

  • synchronizedReentrantLock
  • 递归锁,一种技术,任意线程在获取锁之后能够再次获取该锁,而不会被锁阻塞
  • 原理,通过组合自定义同步器来实现锁的获取和释放,多层锁
  1. 再次获取锁,识别获取锁的线程是否是当前占据锁的线程,如果是,则再次获取,获取锁后,计数+1(ReentrantLock中的state)
  2. 释放锁,计数自减,需要减到0,才能释放
  • 作用,避免死锁
  • 可重入锁加了两把,但是只释放了一把锁,会出现什么问题

程序卡死,线程不能出来,申请了几把锁,需要释放几把

  • 只加了一把锁,但是释放两次

报异常java.lang.IllegalMonitorStateException

读写锁

  • 是一种技术,通过ReentrantReadWriteLock实现
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁,读是无阻塞的
  • 多个读锁不互斥,读锁与写锁互斥
  1. 读锁,允许多个线程获取读锁,允许同时访问同一个资源
  2. 写锁,只允许一个线程获取写锁,不允许同时访问写同一个资源

分段锁

  • 一种机制,ConcurrentHashMap image.png
  • 原理,内部细分若干个小的HashMap(段 Segment),默认情况分为16个段,也是锁的并发度,当需要添加一项key-value时,先根据hashcode获得该key-value应该放到哪个段,然后对该段加锁,完成put操作

在多线程环境下,如果多个线程同时进行put操作,只要被加入的key-value不放在一个段中,就可以做到真正的并行

  • 线程安全,ConcurrentHashMap是一个Segment数组,Segment通过继承ReentrantLock来进行加锁,所以每次需要加锁的操作锁住的是一个Segment,这 样只要保证每个Segment是线程安全的,也就实现了全局的线程安全 image.png

共享锁、独占锁、互斥锁和同步锁

  • 共享锁
  1. RenntrantReadWriteLock、CAS
  2. 一种思想,可以有多个线程获取读锁,以共享的方式持有锁
  3. 和乐观锁,读写锁同义
  • 独占锁
  1. RenntrantLocksynchronized
  2. 一种思想,只能有一个线程获取锁,以独占的方式持有锁
  3. 和悲观锁,互斥锁同义
  • 互斥锁
  1. 与悲观锁、独占锁同义
  2. 某个资源只能被一个线程访问,其他线程不能访问
  3. synchronized
  • 同步锁
  1. 与悲观锁、独占锁同义
  2. 并发执行的多个线程,在同一个时间只允许一个线程访问共享数据
  3. synchronized

分布式锁

  • 分布式应用访问同一个数据库,会造成并发问题,而一个jvm应用只能保证一个应用内的线程安全

对于redis

  • 借助redissetnx方法的特点,用协议的方式设置一个锁key
setnx key val //操作前上锁
del key //操作完后解锁
  • 为了避免意外情况应用崩掉,无法及时释放锁,设置key的超时时间
expire key seconds
  • 但是由于存在业务代码执行不确定,无法确定超时时间,会导致业务还没有执行完毕,锁已经被释放掉,导致其他线程进行访问,执行完后,释放掉前一个线程的锁。

可以为所有锁的val加上一个id,在释放锁时需要进行判断,当前线程只能释放当前线程加的锁

setnx key uuid
  • 但是判断和删除并非原子操作,如果在判断结束,删除前,key过期,导致其他线程马上访问,需要处理超时时间
  1. 锁续命,请求加锁成功,同时开一个分线程,执行定时任务,定时检查业务线程是否还持有锁,将锁的过期时间重新设置,当没有持有锁了,分线程也停掉
  • redisson操作redis的客户端,针对分布式场景 IMG_C00BE76F7AF2-1.jpeg
  • redission底层通过lua脚本(保证原子性),实现上锁逻辑的原子性操作
tryLockInnerAsync //实现上锁
renewExpiration //实现锁续命
unlockInnerAsync //实现释放锁
  • 但是,由于上锁了,面对高并发场景会有性能问题,其他线程只能等待

分布式锁把并行的请求串行化执行,类似ConcurrentHashMap的分段锁实现,如果有100个商品,将其分为10组,就可以让10个请求同时方法,可以通过实现负载均衡,灵活合并分组情况

  • 对于redis主从架构,当线程1在master创建了key,但是master还没有将key更新到slave时,master就崩了,导致锁失效
  1. zookeeper树形存储架构,redis高并发效果比zookeeper好,先同步,再返回,有半数同步成功之后才返回客户端,在选举时,会优先选举同步成功的子节点作为leader
  2. 通过redlock解决,建立一个没有主从关系的redis节点的结构,超过半数的redis节点加锁成功,才算加锁成功,RedissonRedLock可以实现
  • CAP原则,一致性(Consistency)可用性(Availability)分区容错性(Partition tolerance),最多实现两点

redis实现AP,zookeeper实现CP

死锁

  • 一种现象,如果线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获取不到对方的资源,就会造成死锁 image.png
  • Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应

锁行为

锁升级

  • 最开始仅有一个线程运行同步代码时,为偏向锁,当有多个线程来争抢锁了,需要进行锁升级,升级到轻量级锁,当有大量的线程来争抢,导致大量线程进行空转,升级到重量级锁,将线程放入队列
  • 具体实现上,当轻量级锁,自旋次数达到自适应自旋数,就会放到队列中,转换为重量级锁
  • 1.8时,两个线程就有机会升级为重量级锁

延迟偏向

  • 偏向锁默认延时初始化,偏向锁初始化之前,基于类生成的对象都是无锁对象,偏向锁初始化之后,基于类生成的对象都是匿名偏向锁
  • jvm参数默认延时4秒开启偏向锁,-XX:BiasedLockingStartupDelay=0取消延时;-XX:UseBiasedLocking=false关闭偏向锁
  • jvm在初始化时,需要创建很多类和对象,而偏向锁初始化需要在安全点进行,如果在开始就初始化,会导致jvm初始化非常慢
  • 在四秒前创建的对象,成为锁对象后,直接升级为轻量级锁
  • 代码案例
Test test = new Test();
System.out.println(ClassLayout.parseInstance(test).toPrintable());
Thread.sleep(4000);
Test test2 = new Test();
System.out.println(ClassLayout.parseInstance(test).toPrintable());
System.out.println(ClassLayout.parseInstance(test2).toPrintable());

image.png

无锁膨胀成轻量级锁

  • 程序直接执行,此时偏向锁还没有初始化,只能膨胀成轻量级锁
Test test = new Test();
System.out.println(ClassLayout.parseInstance(test).toPrintable());
synchronized (test) {
    System.out.println(ClassLayout.parseInstance(test).toPrintable());
}

image.png

锁粗化

  • 一种技术,如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体之中,就算没有线程竞争,也会导致性能消耗
  1. 把加锁的范围扩展(粗化)到整个操作序列的外部,这样加锁解锁的频率就会大大降低,减少性能损耗
  2. 把连续的、多次的、频繁的加锁行为,合并成一个整体 image.png

锁消除

  • 一种优化技术,把锁干掉,jvm发现某些共享数据不会被线程竞争,就会进行锁消除
  • 如何判断共享数据不会被线程竞争
  1. 逃逸分析,分析对象的作用域,如果A方法定义后,会被作为参数或者返回值,逃出方法,则为方法逃逸;如果能被其他线程访问,则为线程逃逸
  2. 如果堆上某个数据不会逃逸,那么就可以当作栈上数据对象,认为其是线程私有,去掉同步锁

锁降级

  • 针对读写锁,对于数据比较敏感, 需要在对数据修改以后, 获取到修改后的值, 并进行接下来的其它操作

某些操作中,需要更新完数据,立即获得,防止这个过程中,有其他线程进行修改

  • 顺序
rw.writeLock().lock();
System.out.println("rw.writeLock().lock();");
rw.readLock().lock();
System.out.println("rw.readLock().lock();");
rw.writeLock().unlock();
rw.readLock().unlock();
  • 写锁是独占锁,获得写锁时,可以进行锁降级,获得读锁,但是有读锁时,写锁必须等所有读锁结束之后才能获得
  • 以下会阻塞
rw.readLock().lock();
System.out.println("rw.readLock().lock();");
rw.writeLock().lock();
System.out.println("rw.writeLock().lock();");

hashcode对锁的影响

  • 调用hashcode()才生成hashcode
  • 对于多层sync,轻量级锁的内层BasicLock为null,不能回到无锁状态,在此作为锁对象时,只能膨胀成重量级锁
  • 如果是程序启动四秒后创建的对象,会天然的带上匿名偏向锁的标识,如果调用hashcode()方法,会撤销为无锁状态,需要对象头中的hashcode
  • 如果是已偏向锁被线程获取,在同步代码中调用hashcode()方法,会直接膨胀成重量级锁,偏向锁内没有部分可以存hashcode,也没有hashcode可以复制到轻量级锁的lock record image.png
  • 如果是由无锁状态升级为轻量级锁后,并在同步代码块中调用hashcode(),会升级为重量级锁,原因是轻量级锁的lock record来源于对象头信息的复制,而此时是没有的,只能升级为重量级锁通过monitor获取

偏向锁的批量锁撤销和重偏向

  • 批量锁撤销,存在明显多线程竞争的场景下使用偏向锁是不合适的
  • 批量重偏向,一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作
  • 以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。不会破坏正在使用的锁,会将对应的epoch+1,当下次获得锁的时候,该对象的epoch与class的epoch不同,直接修改改锁对象的ThreadID
  • 当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,撤销为无锁,之后,对于该class的锁,直接走轻量级锁的逻辑

对象头

  • 对象头组成结构
  1. 元数数据区markword
  2. 类型指针,指向元空间的类元信息
  3. 数组长度,数组才有 IMG_01549C8FC808-1.jpeg
  • markword部分
  1. 转化为轻量级锁或重量级锁后,其他信息放到其他地方暂存
  2. 当升级为重量级锁之后,重量级锁指针指向monitor IMG_465D5853D624-1.jpeg
  • 打印对象头信息,需要导包
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
    <scope>provided</scope>
</dependency>