Monitor
Java对象头,以32位虚拟机为例
实现原理
监视器/管程,当使用synchronized给对象上锁时(重量级),对象头的mark word就被设置指向monitor对象的指针
一个对象指向一个monitor,不会多个对象指向同一个monitor,monitor是由操作系统提供的
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的
- WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程
synchronized原理进阶
轻量级锁
一个对象有多个线程要加锁,但是加锁的时间是错开的,可以使用轻量级锁进行优化(00)
轻量级锁对用户是透明的,语法仍是synchronized
假设有两个临界区用同一个对象锁进行加锁操作
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
- 先创建锁记录(lock record)对象,每个线程的栈帧都会包含一个锁记录结构,内部可以存储锁定对象的mark word
- 锁记录的Object reference指向锁对象,并尝试用cas替换object的mark word,将替换前object的mark word值存入锁记录
- cas替换成功后,object中存储了锁记录地址和状态00,表示该线程已给对象加锁
-
cas替换失败,有两种情况
-
其他线程已给该对象加了轻量级锁,此时有竞争,进入锁膨胀过程
-
自己执行了synchronized锁重入,再添加一条锁记录作为重入的计数
-
- 当退出synchronized代码块时,如果有值为null的锁记录,表示有重入,这时重置锁记录,重入技术减一
-
当退出synchronized代码块时,如果没有null的锁记录,使用cas将mark word的值恢复给锁对象
- 成功,解锁成功
- 失败,则说明轻量级锁进行了锁膨胀或升级为重量级锁,进入重量级锁解锁流程
缺点
当锁重入时,每次都要用cas去用 锁记录 替换 markword,会影响性能
锁膨胀
当用cas给锁对象加轻量级锁的时候,操作失败,可能是因为已经有其他线程为此对象加上了轻量级锁了,这时就需要进行锁膨胀,将轻量级锁变为重量级锁
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
- 当线程1给锁对象进行轻量级加锁时,线程0已经对该对象加了轻量级锁
-
这时线程1给锁对象加锁失败,进入锁膨胀过程
- 为object申请monitor锁,让object指向重量级锁地址
- 线程1进入monitor的entrylist blocked
- 当线程0解锁后,使用cas将mark word的值恢复给锁对象,失败。这时进入重量级解锁过程,即按照monitor地址找到monitor对象,将owner设置为null,唤醒entrylist中blocked线程
自旋优化
重量级锁竞争时,可以用自旋进行优化,如果当前线程自旋成功(即当前owner值为null,线程释放了锁),则当前线程可以避免阻塞
- 自旋会占用CPU时间,多核自旋才能发挥优势
- java6+后较为智能,如果对象刚刚的一次自旋操作成功过,会认为这次自旋成功的可能性也高,就会多自旋;反之少自旋/不自旋
- java7+后不能控制是否开启自旋功能
偏向锁
轻量级锁的优化
java6引入的偏向锁:只有第一次使用cas将线程id设置到对象的mark word中,之后发现该线程id是自己的就表示没有竞争,不用重新cas,之后只要不发生竞争,该对象就归该线程id对应的线程所有
mark word存的不再是锁记录,而是线程id
- 默认开启偏向锁,创建一个对象时,Mark word的值最后3位为101,而此时thread、epoch、age都为0
- 偏向锁默认是延迟的,不会在程序启动时立即生效,可通过VM参数**-XX:BiasedLockingStartupDelay=0**来禁用延迟
- 若果没有开启偏向锁,对象创建后mark word值最后3位为001,此时的hashcode、age值为0,第一次用到hashcode才会赋值
偏向锁解锁后,线程id仍存储在对象头中
在项目运行时添加-XX:-UseBiasedLocking禁用偏向锁
为什么打印了对象的hashCode后会禁用了对象的偏向锁?
撤销了对象的偏向状态,为了腾出位置给hashCode存值,需要清除线程id的字节码
调用了hashCode后使用偏向锁,得去掉禁用偏向锁的VM参数
当有其他线程使用偏向锁时,会将偏向锁升级为轻量级锁
调用wait/notify撤销偏向锁
批量重偏向
如果对象被多个线程访问,且没有竞争,这时偏向了线程1的对象仍有机会重新偏向线程2,重偏向会重置对象的线程id
当撤销偏向锁的阈值达到了20次后,jvm会给对象加锁时重新偏向至加锁线程
- 先给对象加上偏向锁,偏向t1
- 当t1线程执行完后,t2被唤醒,执行t2线程,由于偏向锁中的线程id还是t1的,所以t2执行时会撤销偏向锁,升级为轻量级锁,当撤销的阈值达到了20次后,对象的偏向状态会转给t2,实现重偏向操作
private static void test3() throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
}
}, "t2");
t2.start();
}
批量撤销
当撤销偏向锁阈值超过40次后,jvm会觉得自己判断失误,不应该偏向,所以整个类的所有对象都会变为不可偏向的,之后新创建的对象也不可偏向的
锁消除
线程默认是启动锁消除优化的,减小加锁带来的性能干扰
java -XX:-EliminateLocks -jar benchmarks.jar关闭锁消除优化
Wait/notify
- Owner线程发现条件不满足时,调用wait方法,即可进入WaitSet变为WAITING状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
- BLOCKED线程会在Owner释放锁时被唤醒
- WAITING线程会在Owner调用notify/notifyAll时被唤醒,但唤醒后不意味着立刻获得锁,仍需进入entryList中重新竞争
原理
必须获得此对象的锁才能调用这几个方法
- obj.wait()让进入object监视器的线程到waitSet等待
- obj.notify()在object上正在waitSet等待的线程中挑一个唤醒
- obj.notifyAll()让object上所有在waitSet等待的线程全部唤醒
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("其它代码....");
}
}).start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
// 主线程两秒后执行
sleep(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
notify的一种结果
notifyAll的结果
- wait()无限制等待,直到notify通知
- wait(long n) 有时限等待,到n毫秒后结束等待/被notify通知
notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为虚假唤醒
sleep()&wait()
不同
- sleep是Thread方法,wait是Object方法
- sleep不会释放锁,wait会释放锁
- sleep 不用强制与synchronized配合使用,wait需要和synchronized搭配一起使用
相同
sleep和wait状态都是TIMED_WAITING
wait()和notify()的正确姿势
用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
解决方法,用 while + wait,当条件不成立,再次 wait
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify();
}
}, "送外卖的").start();
模板
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}