一.Object的wait(long timeout)方法
javaAPI中关于wait(long timeout)放的说明是这样的:导致当前线程等待,直到其他线程调用此对象的java.lang.Object.notify()方法或java.lang.Object.notifyAll()方法,或指定的时间已经过去。
当前线程必须拥有该对象的监视器。
此方法将当前线程(称之为T)放在等待此对象的线程等待集合中,并且释放所有在这个对象上的同步请求。对于线程调度来说,T处于禁用和休眠状态,直到下面4种情况之一发生:
1.其他线程调用对象的notify()方法并且恰好T成为被随意选中的唤醒线程。
2.其他线程调用对象的notifyAll()方法。
3.其他线程打断T
4.指定的休眠时间已经过去,或多或少。如果timeout为零,那么休眠时间将不再生效,T只会简单的等待直到被唤醒。
接着T从等待池中移除并且重新可以被线程调度调用。常规方式下,T接着与其他线程竞争同步对象的权利,一旦T获取对象的控制权,T所有的在这个对象的同步请求将恢复至原点-即wait()方法被调用时的情形(位置)。然后T将从wait()方法调用的地方返回(就是从wait()方法调用出继续执行)。因此,从wait方法返回时,线程T的对象的同步状态,正是因为它是被调用wait方法时的状态。
线程也可以唤醒,并且不通知,中断或超时,所谓的虚假唤醒。而这会很少发生在实践中,应用程序必须防止它通过测试本应导致线程被唤醒的情况下,并继续等待,如果条件不成立。换句话说,等待应该总是发生在循环中,就像这样:
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
(有关此主题的更多信息,请参见第3.2.3节中的Doug Lea的“并行编程在Java中(第二版)”一书(Addison-Wesley出版社,2000),或在约书亚布洛赫的“有效的Java编程语言指南”第50条(艾迪生韦斯利,2001)。
当前线程在等待之前或者正在等待时,如果被其他线程打断,InterruptedException异常将会抛出。直到对象的锁定状态按照上诉的方式恢复,异常才会的抛出。
注意,在等待方法,因为它会将当前线程进入此对象的等待集合,并且解除当前线程对对象的锁定状态;在当前线程T处于等待状态时,T上的其他对象仍然处于锁定状态。
这个方法只能由一个线程是此对象监视器的所有者被调用。看到其中一个线程能够成为监视器所有者的方法的描述notify方法。
二、wait()
调用wait()于调用wait(0)
三、掌握关键点
根据API和这些天对线程的学习,总结了掌握wait方法的需要注意几个关键点:
1.wait方法是使当前线程等待。
2.一个线程可以拥有多个锁,并且当一个线程释放其中一个并等待时仍然可以拥有其他锁。
3.为什么需要使用while循环来防止虚假唤醒。
下面分别通过一段代码对以上几点做解释
1.wait方法是使当前线程等待。
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("end main thread");
}
上面的线程将使main线程处于等待,这样JVM就不会停止,除非其他线程唤醒main线程。当前线程的意思就是执行当前方法的线程。
2.一个线程可以拥有多个锁,并且当一个线程释放其中一个并等待时仍然可以拥有其他锁。
private static Object alock = new Object();
private static Object block = new Object();
public static void main(String[] args) {
Thread at = new Thread(new Runnable() {
@Override
public void run() {
synchronized (alock) {
System.out.println("at获得锁alock");
synchronized (block) {
System.out.println("at获得锁block");
try {
System.out.println("at释放锁alock,但是仍然拥有锁block");
alock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread bt = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (block) {
System.out.println("bt获得锁block");
}
}
});
at.start();
bt.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("bt的状态:"+bt.getState());
}
可以看到执行结果线程bt会是BLOCK(阻塞)状态,原因就是线程at虽然处于等待状态但是at仍然拥有锁block,这样线程bt就无法获取锁block也就无法执行同步块内的方法,这样就会导致bt一直处于阻塞状态。
3.为什么需要使用while循环来防止虚假唤醒。
以前一直想不明白编程语言里边设计个while有什么用处,在写多线程的时候简直就无视它的存在,直到有一天朋友让我找在多线程环境下写的一个生产者消费者模式出的问题,下面是当时的核心代码:
类:ProductStack、2个消费者线程Producer、2个生产者线程Customer。
ProductStack设计思路是提供固定大小的栈来存储产品,当栈满时生产者线程停止,等待消费者消费产品;
当栈空时,消费者停止消费并等待生产者生产产品,并将产品压入ProductStack。
public class ProductStack {
private int maxSize;
private static int DEFAULT_MAX_SIZE = 3;
private Object[] products;
private int stackIndex;
public ProductStack(){
this(DEFAULT_MAX_SIZE);
}
public ProductStack(int maxSize){
this.maxSize = maxSize;
this.products = new Object[this.maxSize];
this.stackIndex = 0;
}
/**
* 入栈
* @param product
* @throws InterruptedException
*/
public synchronized void push(Object product) throws InterruptedException{
if(stackIndex >= maxSize || products == null){
wait();
}
products[stackIndex] = product;
System.out.println("产品" + product + "入栈");
stackIndex++;
notifyAll();
}
/**
* 弹出
* @return Object
* @throws InterruptedException
*/
public synchronized Object pop() throws InterruptedException{
if(stackIndex <= 0 || products == null){
wait();
}
stackIndex--;
Object product = products[stackIndex];
notifyAll();
return product;
}
}
public class Producer extends Thread {
private ProductStack ps;
private int prodInd;
public Producer(ProductStack ps) {
this.ps = ps;
prodInd = 0;
}
@Override
public void run() {
while(!isInterrupted()){//线程中断条件,使用wihle保证线程恢复时对线程可执行的条件重新判断,防止获取到非法条件的数据进而导致程序异常
String product = Thread.currentThread().getId() + "-" + prodInd++;
try {
ps.push(product);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("中断生产者异常");
break;
}
}
System.out.println("end p");
}
}
public class Customer extends Thread{
private ProductStack ps;
public Customer(ProductStack ps) {
this.ps = ps;
}
@Override
public void run() {
while(!isInterrupted()){//线程中断条件,使用wihle保证线程恢复时对线程可执行的条件重新判断,防止获取到非法条件的数据进而导致程序异常
try {
Object product = ps.pop();
System.out.println("线程"+Thread.currentThread().getId()+"消费产品:"+product);
} catch (InterruptedException e) {
System.out.println("中断消费者异常");
break;
}
}
System.out.println("end c");
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
ProductStack ps = new ProductStack();
Producer p1 = new Producer(ps);
Producer p2 = new Producer(ps);
Producer p3 = new Producer(ps);
Customer c1 = new Customer(ps);
Customer c2 = new Customer(ps);
Customer c3 = new Customer(ps);
p1.start();
p2.start();
p3.start();
c1.start();
c2.start();
c3.start();
Thread.sleep(2000);
//在需要的时候中断生产者和消费者线程,不执行中断操作,生产者和消费者将一直执行
p1.interrupt();
Thread.sleep(100);
p2.interrupt();
Thread.sleep(100);
p3.interrupt();
Thread.sleep(100);
c1.interrupt();
Thread.sleep(100);
c2.interrupt();
Thread.sleep(100);
c3.interrupt();
Thread.sleep(1000);
System.out.println("end m");
}
}
我们执行main方法,发现可能会有数组越界的异常,向上向下都有可能越界。看一下数组上标越界的情况,这种情况是以stackIndex=3的某个点抓取的,如下时序图展示的:
当线程p1在等待完毕后再次被唤醒执行时,并没有对stackIndex的值做判断而是直接用这个值去数组products中获取产品,这时候就会产生数组越界的异常。
数组下标越界正好与上面的时序图相反。是在stackIndex=0时开始,两个消费者c1、c2先后进入等待池;然后生产者正常执行,stackIndex=1,之后唤醒c1,c1正常执行后stackIndex=0,之后唤醒c2;c2重新执行又没有判断stackIndex值,这样在stackIndex--后stackIndex变为-1,products[stackIndex]就会下标越界了。