掌握Java并发编程是深入理解Java的必经之路。市面上许多高性能的开源框架中都用到了Java并发编程技术。本专栏从Java并发相关的知识点逐一切入,循序渐进,最终将Java并发相关的知识完美地呈现在读者面前。
首先要讲解的是Object类的用于线程间通信的方法。
wait/notify/notifyAll
谈到并发控制,就不得不提Object类提供的线程间通信方式。Object类提供了三个用于线程间通信的方法:wait/notify/notifyAll。所有的Java对象都自然继承这些方法。下面对这些方法一一讲解。
wait
调用对象的wait()方法会使当前线程等待,直到另一个线程调用此对象的 notify() 方法或 notifyAll() 方法。
调用wait方法时,当前线程必须先拥有此对象的监视器(monitor),否则报错。调用wait方法后,线程释放此监视器的所有权并等待,直到另一个线程通过调用notify方法或notifyAll方法唤醒在此对象监视器上等待的线程。被唤醒的线程与其它线程公平竞争该对象的锁,直到它可以重新获得对象监视器的所有权后才恢复执行。
划重点:对象的wait()方法只能被此对象监视器(monitor)的所有者的线程调用。也就是说当调用wait方法时,首先要确保调用wait方法的线程已经获得了对象的锁。
中断和虚假唤醒是可能的,因此应该向下面示例这样,始终在循环中调用wait方法,而不是使用if判断。
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
wait方法有个重载的版本,即wait(long timeout)
,timeout
为等待的最长时间(以毫秒为单位)
调用wait()方法等价于调用wait(0)方法。换句话说,wait()方法的行为和调用 wait(0) 一样。
调用wait(timeout)方法会导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者指定的时间量已经过去。和wait方法一样,调用wait(timeout)方法时当前线程必须拥有此对象的监视器。
wait(timeout)方法使当前线程(称之为T)将自己置于此对象的等待集(wait set)中,然后放弃对此对象的所有同步声明。 线程T出于线程调度目的而被禁用并处于休眠状态,直到发生以下四种情况之一:
- 某个其它线程调用了此对象的notify方法,而线程T恰好被任意选择为要唤醒的线程。
- 其他一些线程调用了此对象notifyAll方法。
- 其他一些线程中断了线程T 。
- 等待已经过了指定的时间。如果timeout为零,则不考虑等待时间,线程只是等待直到收到通知。
然后,线程T从该对象的等待集中移除,并重新启用线程调度。 然后它以正常的方式与其他线程竞争在对象上同步的权利; 一旦它获得了对象的控制权,它对对象的所有同步声明都将恢复到之前的状态。也就是说,恢复到调用wait方法时的情况。 然后线程T从wait方法的调用中返回。 因此,从wait方法返回时,对象和线程T的同步状态与调用wait方法时完全相同。
线程也可以在没有被通知、中断或超时的情况下唤醒,即所谓的虚假唤醒。 虽然这在实践中很少发生,但应用程序必须再次判断导致线程被唤醒的条件来防止虚假唤醒,如果条件不满足则继续等待。 换句话说,等待应该总是在循环中发生,就像下面的例子一样。
这里使用while循环判断而不是if单次判断,保证满足条件才会继续执行。
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
如果调用对象的wait方法时当前线程没有获得对象的monitor,则会抛出异常,例如下面这个例子。
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
o.wait();
}
}
运行时异常
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.bigbird.conc.Test1.main(Test1.java:6)
当前线程必须拥有该对象的监视器(monitor),才能调用该对象的wait()方法。
获得某个对象的同步监视器可以使用synchronized
关键字,一旦进入到synchronized块内,该线程必然获取到了对象的监视器。
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
synchronized (o){
o.wait();
}
}
}
Tip:sleep()方法和wait()方法的区别
在调用对象的wait()方法时,线程必须持有被调用对象的锁(monitor);调用wait()方法后,线程就会释放掉该对象的锁。而在调用线程的sleep()方法时,不需要获得对象的monitor,也不会释放任何该线程持有的对象的锁(monitor)。
notify
调用对象的notify()方法会唤醒一个正在等待这个对象的锁的线程。如果有多个线程都在等待这个对象的锁,则会任意选择其中一个将其唤醒。正如前文所述,一个线程可以通过调用该对象的wait()方法来等待该对象的锁。被唤醒的线程不会继续执行,而是和其它线程一样共同竞争该对象的锁。获得对象的锁之后才会继续执行。
和wait()方法一样,调用对象的notify()方法的线程也必须先持有该对象的锁(monitor)。
获取对象的锁(monitor)的方式有下列几种:
-
调用该对象的一个用synchronized修饰的实例方法
-
通过执行一个由synchronized修饰的代码块
-
通过调用类的一个synchronized static 修饰的类方法来获得Class类型对象的锁
一个对象的锁(monitor)同一时刻只能被一个线程拥有。
notifyAll
调用对象的notifyAll()方法会唤醒等待此对象锁(monitor)的所有线程,即该对象等待集合(wait set)中的所有线程被唤醒。
前文已经介绍了,一个线程可以通过调用某个对象的wait()方法使其等待该对象的锁,即进入该对象的等待集合。
被唤醒的线程不会继续执行直到其获得该对象的锁。被唤醒的线程和其它线程相比,在对象锁的竞争上没有什么劣势或者优势,它们公平竞争对象的同步锁(monitor),都有可能成为该对象锁的下一个持有者。
和wait()、notify()方法一样,一个对象的notifyAll()方法只能被持有这个对象的monitor的线程所调用。
获取对象的锁(monitor)的3种方式上面已经介绍,无疑离不开synchronized关键字。
案例
结合一个案例讲解一下上述几个方法的应用。
需求描述:创建一个计数器对象,提供加一和减一方法。创建多个线程调用该对象的加一、减一方法。
并输出这样一个结果序列:10101010…
代码如下:
//计数器对象
public class MyCounter {
private int counter;
public synchronized void increase() throws InterruptedException {
//此处不能用if判断,必须放到循环中判断
while (counter != 0) {
wait();
}
counter++;
System.out.println(counter);
notify();
}
public synchronized void decrease() throws InterruptedException {
while (counter != 1) {
wait();
}
counter--;
System.out.println(counter);
notify();
}
}
//加1线程
public class IncreaseThread extends Thread {
private MyCounter counter;
public IncreaseThread(MyCounter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep((long) (Math.random() * 1000));
counter.increase();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//减1线程
public class DecreaseThread extends Thread {
private MyCounter counter;
public DecreaseThread(MyCounter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep((long) (Math.random() * 1000));
counter.decrease();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
测试
public class MainApp {
public static void main(String[] args) {
MyCounter myCounter = new MyCounter();
IncreaseThread increaseThread1 = new IncreaseThread(myCounter);
IncreaseThread increaseThread2 = new IncreaseThread(myCounter);
DecreaseThread decreaseThread1 = new DecreaseThread(myCounter);
DecreaseThread decreaseThread2 = new DecreaseThread(myCounter);
increaseThread1.start();
increaseThread2.start();
decreaseThread1.start();
decreaseThread2.start();
}
}
控制台输出1010101010....
,符合预期。也可以将while循环判断改成if判断,看看结果是否满足预期。
未完待续…请关注公众号【程猿薇茑】