3.8 Monitor 概念
3.8.1 Java 对象头
以 32 位虚拟机为例
普通对象
|--------------------------------------------------------------| | Object Header (64 bits) | |------------------------------------|-------------------------| | Mark Word (32 bits) | Klass Word (32 bits) | |------------------------------------|-------------------------|
- klass word:指向类型元数据的指针。比如 student类型的对象,指向Student类型
数组对象
|---------------------------------------------------------------------------------| | Object Header (96 bits) | |--------------------------------|-----------------------|------------------------| | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | |--------------------------------|-----------------------|------------------------|
其中 Mark Word 结构为
|-------------------------------------------------------|--------------------| | Mark Word (32 bits) | State | |-------------------------------------------------------|--------------------| | hashcode:25 | age:4 | biased_lock:0 | 01 | Normal | |-------------------------------------------------------|--------------------| | thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased | |-------------------------------------------------------|--------------------| | ptr_to_lock_record:30 | 00 | Lightweight Locked | |-------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked | |-------------------------------------------------------|--------------------| | | 11 | Marked for GC | |-------------------------------------------------------|--------------------|
hashcode:每个对象都有一个hashcode值
age:垃圾回收时的分代年龄
biased_lock:是否偏向锁
加锁状态
ptr_to_heavyweight_monitor:指向操作系统中monitor的指针
ptr_to_lock_record:锁记录地址
以Integer对象为例,占 8 + 4 个字节(对象头 + value),基本类型int占 4个字节
3.8.2 原理之 Monitor(锁)
3.8.3 原理之 synchronized
3.8.4 小故事
故事角色
老王 - JVM
小南 - 线程
小女 - 线程
房间 - 对象
房间门上 - 防盗锁 - Monitor
房间门上 - 小南书包 - 轻量级锁
房间门上 - 刻上小南大名 - 偏向锁
批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁(Monitor),当上下文切换时,锁住门。这样,即使他离开了,别人也进不了门,他的工作就是安全的。— 重量级锁
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女晚上用。每次上锁太麻烦了,有没有更简单的办法呢?— 挂书包(轻量级锁)
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式(存在竞争时,锁升级,轻量级 --> 重量级)。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍然觉得麻烦。
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,(偏向锁)下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。(存在竞争时,锁升级,偏向锁 --> 轻量级锁)
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老王(JVM)觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字(批量重偏向)
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包(不可偏向)
3.8.5 原理之 synchronized 进阶
3.9 wait notify
3.9.1 小故事 - 为什么需要 wait
- 由于条件不满足,小南不能继续进行计算
- 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋
直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)
小南于是可以离开休息室,重新进入竞争锁的队列
3.9.2 原理之 wait / notify
Owner 线程获取了锁,但是发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
WAITING线程和BLOCKED线程的区别?
WAITING:已经获得了锁,但是条件不满足,释放锁后进入WaitSet队列;
BLOCKED:没有获得锁,进入EntryList中等待,状态是BLOCKED
==相同点:==都处于阻塞状态,不占用CPU时间片,将来调度时候也不会考虑。
WAITING线程和BLOCKED线程的唤醒条件?
WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
BLOCKED 线程会在 Owner 线程释放锁时唤醒
3.9.3 API 介绍
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
wait:先成为Owner,才有资格进入WaitSet等待。
Notify:必须成为Owner,才能叫醒隔壁的WaitSet中的线程。
**案例一:**当没有获得锁,就使用wait/notify/notifyAll方法时,报IllegalMonitorStateException
@Slf4j(topic = "c.Test18") public class Test18 { static final Object lock = new Object(); public static void main(String[] args) { // synchronized (lock) { try { lock.wait();//IllegalMonitorStateException } catch (InterruptedException e) { e.printStackTrace(); } // } } }
**案例二:**演示Notify和NotifyAll区别
@Slf4j(topic = "c.TestWaitNotify") public class TestWaitNotify { final static Object obj = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (obj) { log.debug("执行...."); try { obj.wait(); // 让线程在obj上一直等待下去 } catch (InterruptedException e) { e.printStackTrace(); } log.debug("其它代码...."); } },"t1").start(); new Thread(() -> { synchronized (obj) { log.debug("执行...."); try { obj.wait(); // 让线程在obj上一直等待下去 } catch (InterruptedException e) { e.printStackTrace(); } log.debug("其它代码...."); } },"t2").start(); // 主线程两秒后执行 sleep(2); log.debug("唤醒 obj 上其它线程"); synchronized (obj) { obj.notify(); // 唤醒obj上一个线程 // obj.notifyAll(); // 唤醒obj上所有等待线程 } } }
notify 的一种结果
15:38:17.518 c.TestWaitNotify [t1] - 执行.... 15:38:17.520 c.TestWaitNotify [t2] - 执行.... 15:38:19.519 c.TestWaitNotify [main] - 唤醒 obj 上其它线程 15:38:19.519 c.TestWaitNotify [t1] - 其它代码....
notifyAll 的结果
15:37:19.908 c.TestWaitNotify [t1] - 执行.... 15:37:19.910 c.TestWaitNotify [t2] - 执行.... 15:37:21.908 c.TestWaitNotify [main] - 唤醒 obj 上其它线程 15:37:21.908 c.TestWaitNotify [t2] - 其它代码.... 15:37:21.908 c.TestWaitNotify [t1] - 其它代码....
3.9.4 API源码查看
wait() 和wait(long n) 的区别
wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
public final native void notify(); public final native void notifyAll(); public final void wait() throws InterruptedException { wait(0); } public final native void wait(long timeout) throws InterruptedException; //本地方法 public final void wait(long timeout, int nanos) throws InterruptedException { if (timeout < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } //可以看出wait(long timeout, int nanos)的nacos参数是无用的 if (nanos > 0) { timeout++; } wait(timeout);//依旧调用的是wait(long timeout) }