Java多线程系列(四)---线程间通信

多线程之间除了竞争访问同一个资源外,也经常需要互相协作完成一件事情。那么它们怎么协作呢?我们就来介绍Java多线程协作的基本机制wait/notify。我们前面在多线程的基础知识介绍过,我们通过synchronized来实现多个线程同步调用方法或者执行代码块。而多线程之间的协作机制一般都是使用synchronized和wait/notify共同完成线程间协作。

wait/notify机制

我们知道,Java在它的根父类Object类中定义了一些线程协作的基本方法,使得每个对象都可以调用这些方法,这些方法有两类,一类是wait,另一类是notify。

主要的两个wait方法:

1
2
public final void wait() throws InterruptedException
public final native void wait(long timeout) throws InterruptedException;

一个带时间参数,单位是毫秒,表示最多等待这么长时间,参数为0表示无限期等待;一个不带时间参数,表示无限期等待,实际就是调用wait(0)。在等待期间都可以被中断,如果被中断,会抛出InterruptedException。

我们知道,每个对象都有一把锁和等待队列,一个线程在进入synchronized代码块时,会尝试获取锁,如果获取不到则会把当前线程加入到等待队列中,其实,除了用于锁的等待队列,每个对象还有另一个等待队列,表示条件队列,该队列用于线程间的协作。调用wait就会把当前线程放在条件队列上并阻塞,表示当前线程执行不下去了,他需要等待一个条件,这个条件它自己改变不了,需要其他线程改变。当其他线程改变了条件后,应该调用Object的notify方法:

1
2
public final native void notify();
public final native void notifyAll();

notify做的事情就是从条件队列中选一个线程,将其从队列中移除并唤醒。

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Service {
public void testMethod(Object lock){
try{
synchronized (lock){
System.out.println("begin wait()");
lock.wait();
System.out.println(" end wait()");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}

public void synNotifyMethod(Object lock){
try{
synchronized (lock){
System.out.println("begin notify() ThreadName=" + Thread.currentThread().getName() +
" time=" +System.currentTimeMillis());
lock.notify();
Thread.sleep(1000 * 1);
System.out.println("end notify() ThreadName=" + Thread.currentThread().getName() +
" time=" + System.currentTimeMillis());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}

该Service中有两个方法,一个是testMethod方法,包含了wait方法,另一个是synNotifyMethod方法了notify方法,我们首先看一下,wait方法会释放锁的测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ServiceThread extends Thread{
private Object lock;

public ServiceThread(Object lock){
this.lock = lock;
}

@Override
public void run() {
super.run();
Service service = new Service();
service.testMethod(lock);
}
}

测试方法如下:

1
2
3
4
5
6
public void testRun() throws Exception {
Object lock = new Object();
new ServiceThread(lock).start();
new ServiceThread(lock).start();
Thread.sleep(1000 * 4);
}

结果如下:

1
2
begin wait()
begin wait()

很明显结果是执行了2次同步代码块,其执行的原因,就是因为第一个wait之后,释放掉了对象锁,所以第二个线程才会执行同步代码块。

还是利用上面的代码,现在我们看一下,notify方法通知等待的线程, 但是不会立即释放锁的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class NotifyServiceThread extends Thread{
private Object lock;
public NotifyServiceThread(Object lock){
this.lock = lock;
}

@Override
public void run() {
super.run();
Service service = new Service();
service.synNotifyMethod(lock);
}
}

测试的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class NotifyServiceThreadTest extends TestCase {
public void testRun() throws Exception {
Object lock = new Object();
ServiceThread a = new ServiceThread(lock);
a.start();
Thread.sleep(1000);
new NotifyServiceThread(lock).start();
new NotifyServiceThread(lock).start();

Thread.sleep(1000 * 10);
}

}

其结果如下:

1
2
3
4
5
6
begin wait()
begin notify() ThreadName=Thread-1 time=1484302436105
end notify() ThreadName=Thread-1 time=1484302437108
end wait()
begin notify() ThreadName=Thread-2 time=1484302437108
end notify() ThreadName=Thread-2 time=1484302438110

测试方法,首先调用上wait的例子,让ServiceThread线程进入等待状态,然后执行2个含有notify操作的线程,可以看出,第一个notify执行完,wait线程并没有立即开始运行,而是Thread-1继续执行后续的notify方法,直到同步语句块结束,然后wait线程立即得到锁,并继续运行。之后Thread-2开始运行,直到结束,因为已经没有等待的线程,所以不会有后续的等待的线程运行。 这里,可以看出一个细节,竞争锁的线程有3个,一个包含wait线程,两个包含notify线程。第一个notify执行结束,获得锁一定是阻塞的线程,而不是另一个notify的线程。 上面的程序展现了等待/通知机制是如何通过wait和notify实现。

wait的具体过程:

  1. 把当前线程放入条件等待队列,释放对象锁,阻塞等待。线程状态变为WAITING或TIMED_WAITING。
  2. 等待时间到或被其他线程调用notify/notifyAll从条件队列中移除,这时,要重新竞争对象锁:
  • 如果能够获得对象锁,线程状态变为RUNNABLE,并从wait调用中返回。
  • 否则该线程加入对象锁等待队列,线程状态变为BLOCKED,只有在获得锁后才会从wait调用中返回。

wait和sleep的区别:

  • wait使线程进入等待,是可以被通知唤醒的,但是sleep只能自己到时间唤醒。
  • wait方法是对象锁调用的成员方法,而sleep却是Thread类的静态方法
  • wait方法出现在同步方法或者同步代码块中,但是sleep方法可以出现在非同步代码中。

wait使线程进入了阻塞状态,阻塞状态可以细分为3种:

  • 等待阻塞:运行的线程执行wait方法,JVM会把该线程放入等待队列中。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池当中。
  • 其他阻塞: 运行的线程执行了Thread.sleep或者join方法,或者发出I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止,或者超时、或者I/O处理完毕时,线程重新转入可运行状态。

总结

wait/notify方法看上去很简单,但往往难以理解wait等的到底是什么,而notify通知的又是什么,我们需要知道,它们与一个共享的条件变量有关,这个条件变量是程序自己维护的,当条件不成立时,线程调用wait进入条件等待队列,另一个线程修改了条件变量后调用notify,调用wait的线程唤醒后需要重新检查条件变量。从多线程的角度看,它们围绕共享变量进行协作,从调用wait的线程角度看,它阻塞等待一个条件的成立。我们在设计多线程协作时,需要想清楚协作的共享变量和条件是什么,这是协作的核心。

参考

https://www.cnblogs.com/swiftma/p/6421803.html