在上一篇中,我们介绍了Java中的线程的基本概念,我们了解到线程是有很多种状态的,本章,我们就来聊聊线程中的状态是如何进行控制与切换的。Java中提供了很多种方法对线程的状态进行控制以及线程之间的通信,包括wait、notify、notifyAll、sleep,下面我们就来看一下它们之间有什么区别,以及如何使用这些方法进行线程状态的控制与通信。
在Java中可以用wait、notify和notifyAll来实现线程间的通信。举个例子,如果你的Java程序中有两个线程——即生产者和消费者,那么生产者可以通知消费者,让消费者开始消耗数据,因为队列缓冲区中有内容待消费(不为空)。相应的,消费者可以通知生产者可以开始生成更多的数据,因为当它消耗掉某些数据后缓冲区不再为满。
在Object对象中有三个方法wait()、notify()、notifyAll(),它们的用途都是用来控制线程的状态。
该方法用来将当前线程置入休眠状态,直到在其他线程调用此对象的notify()方法或notifyAll()方法将其唤醒。
在调用wait()之前,线程必须要获得该对象的对象级别锁,因此只能在同步方法或同步块中调用wait()方法。进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch结构。
该方法唤醒在此对象监视器上等待的单个线程。如果有多个线程都在此对象上等待,则会随机选择唤醒其中一个线程,对其发出通知notify(),并使它等待获取该对象的对象锁。注意“等待获取该对象的对象锁”,这意味着,即使收到了通知,wait的线程也不会马上获取对象锁,必须等待notify()方法的线程释放锁才可以。和wait()一样,notify()也要在同步方法/同步代码块中调用。
总结两个方法:wait()使线程停止运行,notify()使停止运行的线程继续运行。
说了一大堆概念,可能有点绕,下面我们看两个例子来理解一下这两个方法的具体使用方式。
ThreadNotify类:
public class ThreadNotify {
private Object lock;
public ThreadNotify(Object lock) {
this.lock = lock;
}
public void testNotify() {
try {
synchronized (lock) {
System.out.println("start notify........");
lock.notify();
System.out.println("end notify........");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
ThreadWait类:
public class ThreadWait {
private Object lock;
public ThreadWait(Object lock) {
this.lock = lock;
}
public void testWait() {
try {
synchronized (lock) {
System.out.println("start wait........");
lock.wait();
System.out.println("end wait........");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class ThreadWaitNotifyDemo {
public static void main(String[] args) throws Exception {
Object lock = new Object();
Thread waitThread = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
Thread notifyThread = new Thread(() -> {
ThreadNotify threadNotify = new ThreadNotify(lock);
threadNotify.testNotify();
});
waitThread.start();
/**
* 保证waitThread一定会先开始启动
*/
Thread.sleep(1000);
notifyThread.start();
}
}
执行结果:
start wait........
start notify........
end notify........
end wait........
在上面的例子中,我们创建了两个类,分别是ThreadWait、ThreadNotify,ThreadWait负责让线程进行等待操作,ThreadNotify负责唤醒线程的操作,从上面的例子中,我们可以得知几点信息:
上面的例子中,wait方法与notify方法全部在同步代码块中进行的执行,如果不这样会出现什么样子的效果呢?我们来试一下。我们将同步代码块去掉,再次执行:
start wait........
java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.xuangy.concurrency.practice.ThreadWait.testWait(ThreadWaitNotifyDemo.java:37)
at com.xuangy.concurrency.practice.ThreadWaitNotifyDemo.lambda$main$0(ThreadWaitNotifyDemo.java:11)
at java.lang.Thread.run(Thread.java:745)
start notify........
java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.xuangy.concurrency.practice.ThreadNotify.testNotify(ThreadWaitNotifyDemo.java:56)
at com.xuangy.concurrency.practice.ThreadWaitNotifyDemo.lambda$main$1(ThreadWaitNotifyDemo.java:15)
at java.lang.Thread.run(Thread.java:745)
Process finished with exit code 0
发现两个线程中均抛出了异常,说明如果wait()方法和notify()方法不在同步方法/同步代码块中被调用,那么虚拟机会抛出java.lang.IllegalMonitorStateException。
而抛出的IllegalMonitorStateException异常又是什么?我们可以看一下JDK中对IllegalMonitorStateException的描述:
Thrown to indicate that a thread has attempted to wait on an object's
monitor or to notify other threads waiting on an object's monitor
without owning the specified monitor.
这句话的意思大概就是:线程试图等待对象的监视器或者试图通知其他正在等待对象监视器的线程,但本身没有对应的监视器的所有权。
wait方法是一个本地方法,其底层是通过一个叫做监视器锁的对象来完成的。所以上面之所以会抛出异常,是因为在调用wait方式时没有获取到monitor对象的所有权。
因此在执行wait与notify方法之前,必须拿到被调用对象的对象锁,才可以进行等待或唤醒操作。
在上面的例子中,我们多次强调了锁的问题,那么执行wait()方法后或notify()后,会释放掉持有对象的对象锁吗?我们通过下面的例子来实验一下:
public class ThreadWaitNotifyLockDemo {
public static void main(String[] args) throws Exception {
Object lock = new Object();
Thread waitThread1 = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
Thread waitThread2 = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
waitThread1.start();
waitThread2.start();
}
}
执行结果:
start wait........
start wait........
在上面的例子中,我们启动两个线程,都去执行线程等待的操作,从执行结果看到,输出了两条“start wait”,这个可以说明,wait()操作会释放掉当前持有对象的锁,否则第二个线程根本不会进入代码块中执行。
OK,我们通过上面的例子得到结论,wait()操作会释放其持有的对象锁,那么notify()操作是否也是一样的呢?我们再通过一个例子来实验一下:
ThreadNotify类:
public class ThreadNotify {
private Object lock;
public ThreadNotify(Object lock) {
this.lock = lock;
}
public void testNotify() {
try {
synchronized (lock) {
System.out.println("start notify........" + Thread.currentThread().getName());
lock.notify();
//线程休息两秒
Thread.sleep(2000);
System.out.println("end notify........" + Thread.currentThread().getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class ThreadWaitNotifyLock2Demo {
public static void main(String[] args) throws Exception {
Object lock = new Object();
Thread waitThread1 = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
Thread notifyThread1 = new Thread(() -> {
ThreadNotify threadNotify = new ThreadNotify(lock);
threadNotify.testNotify();
});
Thread notifyThread2 = new Thread(() -> {
ThreadNotify threadNotify = new ThreadNotify(lock);
threadNotify.testNotify();
});
Thread notifyThread3 = new Thread(() -> {
ThreadNotify threadNotify = new ThreadNotify(lock);
threadNotify.testNotify();
});
waitThread1.start();
notifyThread1.start();
notifyThread2.start();
notifyThread3.start();
}
}
执行结果:
start wait........
start notify........Thread-3
end notify........Thread-3
start notify........Thread-2
end notify........Thread-2
start notify........Thread-1
end notify........Thread-1
end wait........
这个例子中,我们启动了四个线程,第一个线程执行等待操作,其他两个线程执行唤醒操作,从执行结果中可以看到,当第一次notify后,线程休息了2秒,如果notify释放锁,那么在其sleep的时候,必然会有其他线程争抢到锁并执行,但是从结果中,可以看到这并没有发生,由此可以说明notify()操作不会释放其持有的对象锁。
通过上面的几个例子,我们现在已经知道了wait()应该永远在被synchronized同步代码块或同步方法中进行调用,而需要着重注意的一点是:应该永远在while循环,而不是if语句中调用wait。
为线程是在某些条件下等待的————我们在实际开发中往往会设定一些条件,而使线程进入等待,这个时候你可能直觉就会写一个if语句。但if语句存在一些微妙的小问题,导致即使条件没被满足,你的线程你也有可能被错误地唤醒。所以如果你不在线程被唤醒后再次使用while循环检查唤醒条件是否被满足,你的程序就有可能会出错。
所以,根据JDK给出的代码示例,应该这样去使用wait():
synchronized (obj) {
while (not hold>)
obj.wait();
// Perform action appropriate to condition
// Do something......
}
在while循环里使用wait的目的,是在线程被唤醒的前后都持续检查条件是否被满足。如果条件并未改变,wait被调用之前notify的唤醒通知就来了,那么这个线程并不能保证被唤醒,有可能会导致死锁问题。
interrupt()来自于Thread类,用途是中断线程。如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。
我们来看一下在执行wait()后进行interrupt的效果:
public class ThreadWaitInterruptDemo {
public static void main(String[] args) {
Object lock = new Object();
Thread waitThread = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
waitThread.start();
waitThread.interrupt();
}
}
执行结果:
start wait........
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.xuangy.concurrency.practice.ThreadWait.testWait(ThreadWait.java:18)
at com.xuangy.concurrency.practice.ThreadWaitInterruptDemo.lambda$main$0(ThreadWaitInterruptDemo.java:11)
at java.lang.Thread.run(Thread.java:745)
notifyAll也是来自于Object类的方法,其作用是唤醒在此对象监视器上等待的所有线程。其用法与notify()基本一致,只不过它会唤醒一个对象监视器上等待的全部线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
我们来看一下它的使用方式:
public class ThreadNotifyAllDemo {
public static void main(String[] args) throws Exception {
Object lock = new Object();
Thread waitThread1 = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
Thread waitThread2 = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
Thread waitThread3 = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
Thread waitThread4 = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
Thread waitThread5 = new Thread(() -> {
ThreadWait threadWait = new ThreadWait(lock);
threadWait.testWait();
});
waitThread1.start();
waitThread2.start();
waitThread3.start();
waitThread4.start();
waitThread5.start();
Thread.sleep(2000);
synchronized (lock) {
lock.notifyAll();
}
}
}
执行结果:
start wait........Thread-1
start wait........Thread-2
start wait........Thread-3
start wait........Thread-0
start wait........Thread-4
end wait........Thread-4
end wait........Thread-0
end wait........Thread-3
end wait........Thread-2
end wait........Thread-1
从执行结果可以看到,notifyAll是不会保证唤醒的顺序的,是虚拟机随机指定的。
需要注意的是,在执行notifyAll之前,同样需要获取到对象的锁,即必须在同步方法或者同步代码块中执行,否则会抛出IllegalMonitorStateException异常。
sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,在上述的例子中也用到过,比较容易理解。唯一需要注意的是其与wait方法的区别。
最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。
来看一个简单的例子:
public class ThreadSleepDemo {
public static void main(String[] args) {
Thread sleepThread1 = new Thread(() -> {
System.out.println("当前线程是:" + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("休息两秒结束……");
});
Thread sleepThread2 = new Thread(() -> {
System.out.println("当前线程是:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("休息一秒结束……");
});
sleepThread1.start();
sleepThread2.start();
}
}
执行结果:
当前线程是:Thread-0
当前线程是:Thread-1
休息一秒结束……
休息两秒结束……
本篇中我们学习了wait、sleep、notify、notifyAll的使用方法和机制,对于Object类中的每一个方法,都是非常重要和精妙的,因此想使用好wait与notify、notifyAll我们需要深入的理解其机制,才能真正的使用好这些方法。
由于个人水平非常有限,对这几个方法的使用也是非常的粗浅,示例中如果有不合理的地方,希望读者多多指正,下一篇中,我们将介绍一下Java中用来实现线程同步的关键字synchronized,敬请期待!