在多线程机制中,线程之间需要传输信息。一般有以下几种通信机制:
- 共享对象:通过在共享对象中设置信号量,多个线程通过读取、修改该信号量来通信。
- wait/notify()方法:线程之间通过调用wait()、notify()方法实现线程等待、唤醒状态,从而达到线程通信的目的。
接下来我们分别看看这两种方法:
通过共享对象通信
在共享对象中设置信号量是最简单也是最常用的线程通信方法。共享变量需要使用volatile
修饰。我们知道,volatile
可以保证所修饰的变量立即被其他线程看到,而这种特性正是我们需要的。
下面是一个例子
public class TestShared {
private volatile boolean flag =false; //共享变量
//用来保证主线程等待运行线程结束后再退出
private CountDownLatch countDownLatch = new CountDownLatch(2);
private void test() {
new Thread(() -> {
for (int i=0;;i++) {
if (flag) { //当共享变量为true时,停止循环
break;
}
System.out.println(i+"-------->"+Thread.currentThread().getName());
}
countDownLatch.countDown();
},"thread-one").start();
}
private void test2(){
new Thread(() -> {
try {
Thread.sleep(3000);
flag=true; //3秒后将共享变量设为true
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
},"thread-two").start();
}
public static void main(String[] args) {
TestShared testShared = new TestShared();
testShared.test();
testShared.test2();
try {
testShared.countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
以上例子应该很好理解,利用共享变量flag
控制thread-one
线程的停止。即完成了线程之间的通信。
这里需要说明的是,你会发现,即使你的共享变量没有用
volatile
修饰,线程one也会在规定时间停止。是的。volatile的作用是保证修饰变量对其他线程的可见性。但这并不表明,你不用volatile
修饰,其他线程就看不到这个变量了。你最终都会看到这个变化,只是volatile
会让你立马看到修改的结果。
wait、notify()方法
java内置了一个等待机制实现线程之间的通信,在Object对象中有wait()、notify()、及notifyAll()3个方法。
在一个线程中调用了某对象wait()
后,该线程就将处于等待状态。直到另一个线程调用同一对象的notify()
才能继续运行。这样我们就可以据此实现线程通信。
另外需特别注意的是:为了调用某对象的wait、notify
方法,你需要获取这个对象的锁。这是必须的。在实现上,JVM会检查调用wait
的线程是否同时是锁的拥有者,否则就抛出异常。
同样,我们来看一个例子:
public class WaitNotifyTest {
/**
* wait、notify用法
* 当执行一个对象的wait()、notify()方法时,必须持有这个对象的锁,即在同步块中调用方法
* 在JVM实现中,当执行一个对象的wait()方法时,会首先检查它是否在同步块中,否则抛出异常
* @param args args
*/
public static void main(String[] args) {
NotifyObject notifyObject = new NotifyObject();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"开始");
for(int i=0;i<100;i++){
if(i==50){
//需获得这个对象的锁才能执行wait()方法
synchronized (notifyObject){
try {
notifyObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println( Thread.currentThread().getName()+":"+i);
}
}).start();
new Thread(()->{
for(int i=0;i<500;i++){
System.out.println( Thread.currentThread().getName()+":"+i);
}
synchronized (notifyObject){
notifyObject.notify();
}
}).start();
}
}
class NotifyObject {
}
当第一个线程运行到i=50
后,进入阻塞状态。直到第二个线程执行完毕,线程一才开始继续执行。
wait、notify机制要记得在执行该方法时,必须获得了该对象的锁。所以,为了使用方便,我们可以对其进行一定封装。
public class WaitNotify2Test {
private final NotifyObject notifyObject = new NotifyObject();
public void doWait() {
synchronized (notifyObject) {
try {
notifyObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void doNotify() {
synchronized (notifyObject) {
notifyObject.notify();
}
}
public static void main(String[] args) {
WaitNotify2Test waitNotify2Test = new WaitNotify2Test();
new Thread(() -> {
for(int i=0;i<100;i++){
if(i==50){
waitNotify2Test.doWait();
}
System.out.println( Thread.currentThread().getName()+":"+i);
}
}).start();
new Thread(()->{
for(int i=0;i<500;i++){
System.out.println( Thread.currentThread().getName()+":"+i);
}
waitNotify2Test.doNotify();
}).start();
}
}
NotifyObject
类即是上面示例的内部类。通过对wait、notify
的封装,我们可以使用更方便。
另外,还有一点需要注意:
不要在字符串常量或全局变量中调用wait、notify()方法。
这是因为,如果你的程序中有多套wait、notify线程,都使用一个字符串常量作为监视器的对象,则会出现假唤醒的情况。