线程通过在共享对象中发送一个信号实现与其他线程通信。如下图所示,设置一个成员变量 hasProcess,线程A通过setHasProcess同步方法设置hasProcess的值,这样线程B可以读取到成员变量hasProcess的值,实现线程之间的通信。
public class MySignal {
private boolean hasProcess = false;
public synchronized void setHasProcess(boolean hasProcess) { this.hasProcess = hasProcess; }
public synchronized boolean getHasProcess() { return this.hasProcess; }
}
线程A和线程B必须引用共享对象 MySignal 的实例来实现线程A与线程B之间的线程通信。如果线程A和线程B引用了不同的MySignal实例对象,那么线程A和线程B不会检测到彼此发送的信号,达不到线程通信的目的。
上例中线程B需要等待信号处理数据,因此它的业务代码很可能一直在等待信号,浪费了很多CPU资源。
MySignal singal = ...
while (!singal.getHasProcess()) {
//do nothing... busy waiting
}
因为我们不知道线程A什么时候发送信号,因此导致线程B一直等待线程A的信号,没做任何事情,浪费CPU资源。
Busy waiting 浪费了CPU资源,即使某些情况下等待的时间非常短暂。因此,让等待信号的线程在接收信号之前处于无效状态,直到接收信号之后(被唤醒)继续执行代码,这是一种非常聪明的做法。
**等待通知机制:**让等待信号的线程在接收信号之前处于无效状态(不占用CPU资源,线程暂停执行),由发出信号的线程唤醒等待信号的线程,等待信号的线程被唤醒后继续执行,这就是等待通知机制。
Java 有一个等待机制:让等待信号的线程处于无效状态。Java Object 类定义了三个方法分别是wait()、notify()、notifyAll(),通过这三个方法我们可以实现这种机制。
一个线程调用了某个对象的 wait() 方法之后,这个线程就成为无效状态,直到另外一个线程调用了同一个对象的 notify() 方法或者 notifyAll()方法之后,原来处于无效状态的线程才结束无效状态。
线程调用某个对象的 wait() 方法或者 notify() 方法必须获取这个对象的锁,也就是说 wait() 方法或者 notify() 方法调用的代码必须包含在 synchronized 代码块中,synchronized 监视器必须是 wait() 或 nofity() 方法所属的那个对象。
示例:
//监视器对象
public class MonitorObject {
}
public class MyWaitNotify {
MonitorObject myMonitorObject = new MonitorObject();
public void doWait() {
synchronized (myMonitorObject) {
try {
//wait()方法必须包含在synchronized代码块中,且synchronized监视器是wait()方法所属对象myMonitorObject
myMonitorObject.wait();
} catch (InterruptedException e) {
...
}
}
}
public void doNotify() {
synchronized (myMonitorObject) {
//notify()方法必须包含在synchronized代码块中,且synchronized监视器是notify()方法所属对象myMonitorObject
myMonitorObject.notify();
}
}
}
当一个线程调用某个对象的notify()方法,会唤醒一个等待状态的线程;通过调用notifyAll()方法,唤醒所有处于等待状态的线程。
你可以从上例中观察到wait()方法和notify()方法都在同步代码块中,并且同步代码块的监视器对象与调用wait方法(或notify方法)的对象是同一个对象。这是强制的,一个线程不能在没有持有某个对象锁的方法上调用wait方法或notify方法,否则程序将抛出 IllegalMonitorStateException (非法监视器状态异常)。
你可能会想:当等待线程进入synchronized代码块调用wait方法使得线程进入等待状态,并没有退出synchronized代码块,那么等待线程就会阻止唤醒线程进入synchronized代码块调用notify()方法,那么唤醒线程怎么可能能够进入synchronized代码块呢?答案是唤醒线程可以进入synchronized代码块调用notify方法,原因是当一个线程调用了wait方法之后,当前线程会释放基于某个对象的持有锁,这样其他的线程就有机会进入synchronized代码块。
一旦一个线程被唤醒,它不能立即退出wait()方法,等待线程需要等待唤醒线程退出包含notify()方法的synchronized代码块之后,等待线程需要重新获取对象的持有锁之后退出wait方法,然后继续执行下面的代码。
使用notifyAll()唤醒多个等待线程,那么多个等待线程也需要等唤醒线程退出synchronized代码块之后,各个等待线程需要重新获取对象的持有锁才能退出wait()方法继续执行程序,由于各个等待线程的synchronized代码块的监视器是同一个对象,因此各个线程之间是同步退出wait()方法。
小结:
当一个线程调用notify方法时没有线程处于等待状态,那么这个唤醒信号就被丢失了。丢失信号可能会导致程序产生一些问题,也可能不会产生一些问题,但我们需要知道这种情况可能发生。在某些情况下,丢失信号可能导致一些等待线程一直处于等待中,从未被唤醒,因为唤醒线程调用notify方法发生在等待线程调用wait方法之前。
为了避免这个问题,我们可以将信号保存在共享数据对象中,这里就不在举例了。
请看示例:
/**
* 程序入口
*
* @author : sungm
* @date : 2020-09-01 17:00
*/
public class Main {
public static int number = 0;
public static void main(String[] args) {
Lock lock = new Lock();
Runnable myRunnable = () -> {
try {
lock.lock();
number++;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unLock();
}
};
new Thread(myRunnable, "Thread A").start();
new Thread(myRunnable, "Thread B").start();
new Thread(myRunnable, "Thread C").start();
}
}
/**
* 自定义锁
*
* @author : sungm
* @date : 2020-09-01 16:21
*/
public class Lock {
private boolean hasLocked = false;
public synchronized void lock() throws InterruptedException {
if (hasLocked) {
wait();
}
hasLocked = true;
}
public synchronized void unLock() {
hasLocked = false;
notify();
}
}
现在我们来分析下上面代码中可能会存在什么问题?
首先假设线程A调用了lock()方法获取到了锁,然后执行number++操作时,此时线程B进入lock()方法(锁已被线程A获取),因此线程B调用wait()方法进入等待状态,之后线程A调用了unLock()方法释放锁资源并唤醒一个线程,因为这里只有线程B处于wait状态,因此线程B被唤醒,线程B等待重新进入synchronized代码块,若此时线程C优先于线程B进入lock同步方法,锁归线程C所有,当线程C退出lock方法后,线程B进入synchronized退出wait方法,继续执行下面的程序代码,那么此时锁同时被线程B和线程C拥有,出现了不同步的操作,这样容易导致程序出现问题。
现在我们来解决上面代码存在的问题
我们只要将lock()方法中的if(hasLocked)判断条件换成while(hasLocked)代码块,就能很好的解决上面这个问题,while(hasLocked)我们称它为“自旋锁”。如下所示:
public synchronized void lock() throws InterruptedException {
while (hasLocked) {
wait();
}
hasLocked = true;
}
这样,当线程B退出wait()方法后继续判断hasLocked条件是否为真,仅当锁没有被任何线程锁拥有时才真正的唤醒线程,否则线程继续等待。
自旋锁在处理多个线程等待同一个信号时也是一种很好的方案,我们会使用notifyAll()方法唤醒等待的所有线程,同时只有一个线程能够退出wait()方法,当某个线程退出wait()方法后,这个线程会修改hasLocked的值,当其他线程退出wait方法时会自旋判断hasLocked,若锁被其他线程拥有,会继续进入等待状态,从而避免程序产生一些不正常的操作。