Java多线程(二)——Java对象的Monitor机制
一、概述
Java虚拟机给每个对象和class字节码都设置了一个监听器Monitor,用于检测并发代码的重入,同时在Object类中还提供了notify和wait方法来对线程进行控制。
在java.lang.Object类中有如下代码:
public class Object {
...
private transient int shadow$_monitor_;
public final native void notify();
public final native void notifyAll();
public final native void wait() throws InterruptedException;
public final void wait(long millis) throws InterruptedException {
wait(millis, 0);
}
public final native void wait(long millis, int nanos) throws InterruptedException;
...
}
二、Monitor机制
Monitor的机制如下图:

结合上图来分析Object的Monitor机制。
Monitor可以类比为一个特殊的房间,这个房间中有一些被保护的数据,Monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有Monitor,退出房间即为释放Monitor。
当一个线程需要访问受保护的数据(即需要获取对象的Monitor)时,它会首先在entry-set入口队列中排队(这里并不是真正的按照排队顺序),如果没有其他线程正在持有对象的Monitor,那么它会和entry-set队列和wait-set队列中的被唤醒的其他线程进行竞争(即通过CPU调度),选出一个线程来获取对象的Monitor,执行受保护的代码段,执行完毕后释放Monitor,如果已经有线程持有对象的Monitor,那么需要等待其释放Monitor后再进行竞争。
再说一下wait-set队列。当一个线程拥有Monitor后,经过某些条件的判断(比如用户取钱发现账户没钱),这个时候需要调用Object的wait方法,线程就释放了Monitor,进入wait-set队列,等待Object的notify方法(比如用户向账户里面存钱)。当该对象调用了notify方法或者notifyAll方法后,wait-set中的线程就会被唤醒,然后在wait-set队列中被唤醒的线程和entry-set队列中的线程一起通过CPU调度来竞争对象的Monitor,最终只有一个线程能获取对象的Monitor。
需要注意的是:
- 当一个线程在wait-set中被唤醒后,并不一定会立刻获取Monitor,它需要和其他线程去竞争
- 如果一个线程是从wait-set队列中唤醒后,获取到的Monitor,它会去读取它自己保存的PC计数器中的地址,从它调用wait方法的地方开始执行。
三、Monitor的实现
前面已经分析了Monitor的机制,那么在Java中是如何实现的呢?
即通过synchronized关键字实现线程同步来获取对象的Monitor。synchronized同步分为以下两种方式:
同步代码块
synchronized(Obejct obj) {
//同步代码块
...
}
上述代码表示在进入同步代码块之前,先要去获取obj的Monitor,如果已经被其他线程获取了,那么当前线程必须等待直至其他线程释放obj的Monitor
这里的obj可以是类.class,表示需要去获取该类的字节码的Monitor,获取后,其他线程无法再去获取到class字节码的Monitor了,即无法访问属于类的同步的静态方法了,但是对于对象的实例方法的访问不受影响
同步方法
public class Test {
public static Test instance;
public int val;
public synchronized void set(int val) {
this.val = val;
}
public static synchronized void set(Test instance) {
Test.instance = instance;
}
}
上述使用了synchronized分别修饰了非静态方法和静态方法。
非静态方法可以理解为,需要获取当前对象this的Monitor,获取后,其他需要获取该对象的Monitor的线程会被堵塞。
静态方法可以理解为,需要获取该类字节码的Monitor(因为static方法不属于任何对象,而是属于类的方法),获取后,其他需要获取字节码的Monitor的线程会被堵塞。
四、Object的notify方法和wait方法详解
上面在讲述Monitor机制的时候已经分析了notify和wait的用法,这里具体分析下。
wait方法
wait有三个重载方法,分别如下:
wait()
wait(long millis)
wait(long millis, int nanos)
后面两个传入了时间参数(nanos表示纳秒),表示如果指定时间过去还没有其他线程调用notify或者notifyAll方法来将其唤醒,那么该线程会自动被唤醒。
调用obj.wait方法需要注意的是,当前线程必须获取到了obj的Monitor,才能去调用其wait方法,即wait必须放在同步方法或同步代码块中。(调用的是obj.wait(),而不是Thread.currentThread.wait())
notify方法
notify有两个方法notify和notifyAll,前者只能唤醒一个正在等待这个对象的monitor的线程,具体由JVM决定,后者则会唤醒所有正在等待这个对象的monitor的线程
需要关注的点:
- 调用notify方法,并不意味着释放了Monitor,必须要等同步代码块结束后才会释放Monitor
- 在调用notify方法时,必须保证其他线程处于wait状态,否则调用notify没有任何效果,导致之后其他线程永远处于堵塞状态