JAVA并发编程(五)-线程间协作

Scroll Down

JAVA并发编程(五)-线程间协作

等待和通知:wait/notify

在多线程中,可能会出现这种场景,线程的处理条件是基于共享变量的,不符合条件的需要暂停,但是当他符合条件的时候需要使他继续执行剩余代码,这里就需要使用到wait和notify。

一个线程因为其执行目标动作所需要的条件未满足而被暂停的过程称为等待(wait),一个线程更新了共享变量,使得其他的线程所需的条件得到满足并唤醒那些暂停的线程称为通知(notify)

由于同一个对象的同一个方法(object.wait)可以被多个线程执行,因此一个对象可能会存在多个等待线程,而wait方法会使的对应执行线程以原子性释放其拥有的内部锁,而线程暂停的时候其对object.wait()方法并未返回,object.notify()可以唤醒对应object上的**任意一个等待线程,**被唤醒的线程在其获得处理器资源的时候,还需要再次申请内部锁,当被唤醒的线程获得内部锁之后,才允许继续执行object.wait()剩余的指令,知道wait()方法返回.

wait()方法使用

因为只有在不满足条件的时候才会执行wait()方法,当等待线程被唤醒并申请获得内部锁时,可能会被其他线程抢占相应的内部锁并修改共享变量导致线程执行的条件不成立,所以Object.wait()方法返回时都需要判断相关条件是否成立,所以都需要放在循环中,例如

synchronized(Object){
while(条件不成立){
object.wait()
}
doOther();//目标动作
}

而线程对条件的判断以及执行目标动作必须具有原子性,所以目标动作必须和条件判断在同一个临界区中

注意:

  • 等待线程对条件的判断,Object.wait()方法调用都必须放在相应对应内部锁的临界区中的一个循环语句中
  • 等待线程对保护条件的判断,Object.wait()方法以及目标动作执行必须放在同一个内部锁的临界区语句中
  • Object.wait()暂停当前线程时释放的只是与该wait方法所属对象的内部锁,当前线程持有的其他内部锁或者显示锁不会被释放.

notify()方法使用

synchronized(Object){
update条件;
object.notify();//唤醒其他某个线程
或者
object.notifyAll()//唤醒所有wait线程
}

在使用通知方法时,需要持有该方法所属对象的内部锁,因此wait()方法必须要释放其对应内部锁,否则notify线程也无法获得内部锁,所以notify()方法或者notifyALL()方法持有的内部锁只有在完成对应临界区内所有代码才会释放,所以为了能够尽快让等待线程被唤醒后就获得内部锁,我们notify()方法需要尽可能的靠近临界区结束,否则在等待线程被唤醒到去申请锁的时候被其他线程抢占了,又会再次暂停.

notify()方法只能任意唤醒一个等待线程,我们需要使用notifyAll()方法唤醒相应对象上所有的等待线程.

Object.wait()/notify()方法的内部实现

JAVA虚拟机会在为每个对象维护一个入口集(Entry Set)用于存储申请该对象内部锁的线程,还会为每个对象维护一个等待及(Wait Set)队列,用于存储该对象上的等待线程.当调用Object.wait()线程时将当前线程暂停并释放相应内部锁的同时会将当前线程的引用存入该方法所属对象的等待及中.执行一个对线的notify()方法,会使等待及中任意一个线程被唤醒,被唤醒的线程仍然会存放在等待队列中,直到该线程持有对应对象的内部锁(此时Object.wait()还未返回),Object.wait()会使当前线程从其所在的线程等待集移除,接着Object.wait()就返回了.

Object.wait()实现的伪代码如下:

public void wait(){
    //使用wait方法必须已经获取当前对象内部锁
    if(!Thread.hodlerLock()){
        throw new java.lang.IllegalMonitorStateException();
    }
    if(当前对象不再等待集中){
        //将对象加入等待集;
        addWaitSet();
    }
    //原子性操作
    atomic{
        //释放对应内部锁
        releaseLock();
        //暂停当前线程
        block(Thread.currentThread());
    }
    //再次申请当前锁
    acquireLock(this);
    //将当前线程从等待集中移除
    removeFromWaitSet(Thread.currentThread());
    return;
}

wait/notify的开销及存在的问题

问题:

  • 过早唤醒问题,如果一组线程同步在对象someObject之上,T1,T2,T3.当state等于state1的时候满足条件,线程T2,T3是当state等于state2的时候满足保护条件,此时如果通知线程将状态改为了state1,notifyAll()会唤醒所有线程,即使T2,T3不符合条件,使得T2,T3被唤醒之后又需要继续等待,此时称为过早唤醒.
  • 信号丢失问题,一个通知线程多个等待线程,当通知线程没有使用notifyAll()的时候,notify()只能唤醒某个等待线程,导致其他线程都没有被唤醒从而永远无法被唤醒.
  • 欺骗性唤醒问题,等待线程可能在没有任何其他线程执行Object.notify()/notifyAll()的情况下被唤醒,这种现象称为欺骗性唤醒,这种情况下实际出现概率很低,但是JAVA平台和操作系统是允许这种现象产生的,需要将Object.wait()调用行放在循环的保护条件判断中,即使被唤醒也不会造成实际影响.(while(条件){Object.wait()})
  • 上下文切换问题,wait/notify会导致较多的上下文切换
    • 等待线程执行Object.wait()至少会导致对象内部锁的两次申请与释放,通知线程在执行Object.notify/notifyAll()的时候又会导致锁的申请,锁的申请与释放就可能会导致上下文切换
    • 等待线程从暂停到唤醒就会导致上下文切换
    • 被唤醒的等待线程在继续运行时需要申请相应对象的内部锁,而等待线程可能需要和相应对象入口集中的其他线程或者其他新来的活跃线程争用相应内部锁,从而导致上下文切换
    • 过早唤醒问题也会导致上下文切换

wait/notify与Thread.join()

Thread.join()可以使当前线程等待目标线程运行结束后才继续运行,Thread.join()还可以传入时间参数,Thread.join(long time) 即使是目标线程没有运行结束,到达超时时间后也会继续运行,join()实际上使用了wait/notify来实现的

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

如果目标线程还未结束时会调用wait方法来暂停当前线程,直到目标线程已终止,使用Thread.isAlive()来判断,join(long)实际上就是调用了wait(long)方法