线程之间的协作

一、概览

在线程之间共享受限资源中,通过使用锁(互斥)来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。线程之间的协作使得多个任务可以一起工作去解决某个问题。现在问题不是彼此之间的干涉,而是彼此之间的协调,因为在这类问题中,某些部分必须在其他部分被解决之前解决。

任务协作的关键是通过任务之间的握手,握手也是通过互斥这个基础特性来实现。互斥能够确保只有一个任务可以响应某个信号,这样就可以根除任何任务之间的竞争条件。握手可以通过Object的方法wait()和notify()来安全的实现,Java SE5的并发类库还提供了具有await()和signal()方法的Condition对象。

二、wait()和notifyAll()

wait()可以等待某个外部条件发生变化,而改变这个条件超出了当前方法的控制能力。通常这种条件将有另一个任务来改变。我们不希望不断进行空循环,来测试条件是否满足,这种方式称为忙等待,是一种不良的cpu周期使用方式。因此使用wait()将任务挂起,并且只有notify()或notifyAll()发生时,任务才会被唤醒并去检测所产生的变化。因此wait()提供了一种在任务间对活动对象的调用方式。

调用sleep()时锁并没有释放,调用yield()也是这种情况。当任务在方法里遇到wait()的调用时线程执行被挂起,对象上的锁被释放。因为wait()将释放锁,因此另一个任务可以获得锁,因此该对象上的其他synchronized方法可以在wait()期间被调用。这一点至关重要,因为其他的方法通常会产生变化,这种改变正是使被挂起的任务重新唤醒所感兴趣的变化。调用wait()时就是在声明:我已刚刚做完能做的所有事情,因此要在这里等待,但是我希望其他synchronized操作在条件合适的情况下可以执行。

sleep()与wait()的区别是:

  • 在wait()期间对象锁时释放的
  • 可以通过notify()、notifyAll(),或者时间到期,从wait()中恢复。

wait()、notify和notifyAll()是基类Object的一部分,不属于Thread的一部分。不过这是有道理的,因为这些方法操作的锁也是所有对象的一部分,所以可以把wait()放进任何同步控制方法里,而不用考虑这个类是否继承自Thread还是实现了Runnable接口。实际上,我们只能在同步代码块或同步方法里调用wait()、notify和notifyAll(),如果在非同步方法里调用这些方法,能编译通过但是会报IllegalMonitorStateException异常,并伴随一些含糊的信息,比如“当前线程不是拥有者”。消息是意思是任务在调用这些方法前必须“拥有”对象的锁。

实例:Car的涂蜡和抛光程序,涂蜡在抛光之前执行。

public class WaxOMatic {

    public static void main(String[] args) throws InterruptedException {

        Car car = new Car();
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new WaxOn(car));
        service.execute(new WaxOff(car));
        TimeUnit.SECONDS.sleep(5);

    }
}

class Car{
    private boolean waxOn = false;

    public synchronized void waxed(){
        waxOn = true;
        notifyAll();
    }

    public synchronized void buffed(){
        waxOn = false;
        notifyAll();
    }

    public synchronized void waitForWaxing() throws InterruptedException {
        while (waxOn == false){
            wait();
        }
    }

    public synchronized void waitForBuffing() throws InterruptedException {
        while (waxOn == true){
            wait();
        }
    }
}

class WaxOn implements Runnable{

    private Car car;

    public WaxOn(Car car){
        this.car = car;
    }

    @Override
    public void run() {
        try {
        while (!Thread.interrupted()){
            System.out.println("Wax on");
            TimeUnit.MILLISECONDS.sleep(2000);
            car.waxed();
            car.waitForBuffing();
        }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

class WaxOff implements Runnable{

    private Car car;

    public WaxOff(Car car){
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()){
                car.waitForWaxing();
                System.out.println("Wax Off!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.buffed();

            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

实例中用一个检查感兴趣的条件的while循环包围wait()这很重要。这是因为:

  • 可能有多个任务出于相同的的原因等待同一个锁,而第一个唤醒任务可能会改变状况。如果属于这种情况,那么这个任务该被挂起,直至其感兴趣的条件发生变化。
  • 在这个任务从其wait()中被唤醒的时刻,有可能会有某个其他的任务已经做出了改变,从而使得这个任务在此时不能执行,或者执行其操作已显得无关紧要。此时应该再次通过调用wait()将其挂起。
  • 也有可能某些任务出于不同的原因在等待你的对象上的锁。这种情况下,你需要检查是否已经有正确的原因唤醒,如果不是就再次调用wait()。

错失信号

当两个线程使用notify()/wait()或notifyAll()/wait()进行协作时,有可能就会错过某个信号。假设T1是通知T2的线程,而这两个线程都是通过使用下面(有缺陷)方式实现的:

T1:

synchronized(sharedMonitor){
    <set up condition for T2>
    sharedMonitor.notify();
}

T2:

while(someCondition){
    //point1
    synchronized(sharedCondition){
        sharedMonitor.wait();
    }
}

假设T2对someCondition求值发现其为true.在point1,线程调度器可能切换到T1。而T1将执行其设置,然后调用notify()。当T2继续执行时对T2来说时机已经太晚了,以至于不能意识到这个条件已经发生了变化,因此会盲目进入wait()。此时notify()将错失,而T2也将无线等待这个已发送过去的信号,从而产生死锁。

该问题的解决方式为防止在someCondition变量上产生竞争条件,T2正确的执行方式:

 synchronized(sharedCondition){
    while(someCondition){
        sharedMonitor.wait();
    }
}

修改后如果T1先执行,那控制返回T2时它将发现条件发生了变化,从而不会进入wait()。反过来如果T2首先执行,那它将进入wait(),并稍后会有T1唤醒,因此信号不会错过。

三、 notify()和notifyAll()区别

在技术上可以有多个任务在单个对象上处于wait()状态,因此调用notifyAll()比调用notify()更安全。使用notify()而不是使用notifyAll()是一种优化,但是需要注意一下情况:

  • 使用notify()时,在众多需要等待锁的任务中只有一个会被唤醒,因此如果使用notify()就必须确保被唤醒的是恰当的任务。
  • 为了使用notify(),所有任务必须等待相同的条件,因为如果你有多个任务等待不同的条件,你就不会知道是否唤醒了恰当的任务。如果使用notify(),当条件发生变化时,必须只有一个任务从中受益。
  • 这些限制对所有可能存在的子类都必须总是起作用。

如果这些规则中有任何一条不满足你就必须使用notifyAll(),而不是notify()。

一直有一个比较困惑的描述:notifAll()将唤醒“所有正在等待的任务”。这是否意味着程序的任何地方,任何处于wait()状态中的任务都将被任何notifyAll()唤醒呢?显然不是,下面的例子演示了这种情况:

public class NotifyVsNotifyAll {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++){
            executor.submit(new Task());
        }

        executor.execute(new Task2());
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            boolean prod = false;
            @Override
            public void run() {
                if (prod){
                    System.out.println(" notify()");
                    Task.block.prod();
                    prod = false;
                }else {
                    System.out.println(" notifyAll()");
                    Task.block.prodAll();
                    prod = true;
                }
            }
        }, 400, 400);

        TimeUnit.SECONDS.sleep(5);
        timer.cancel();

        System.out.println("Timer canceled");
        TimeUnit.MILLISECONDS.sleep(500);

        System.out.println("Task2.block.prodAll()");
        Task2.blocker.prodAll();
        TimeUnit.MILLISECONDS.sleep(500);

        executor.shutdownNow();

    }
}

class Blocker {
    synchronized void waitingCall() {
        try {
            while (!Thread.interrupted()) {
                wait();
                System.out.println(Thread.currentThread() + " " );
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized void prod(){
        notify();
    }

    synchronized void prodAll(){
        notifyAll();
    }
}

class Task implements Runnable{

    static Blocker block = new Blocker();

    @Override
    public void run() {
        block.waitingCall();
    }
}

class Task2 implements Runnable{

    static Blocker blocker = new Blocker();

    @Override
    public void run() {
        blocker.waitingCall();

    }
}

在Blocker中prod()和prodAll(),这些方法是synchronized的,这意味着它们将获得自身的锁,因此当它们调用notify()或notifyAll()时,只在这个锁上调用是符合逻辑的——因此只唤醒在等待这个特定锁的任务。

生产者消费者与队列

wait()和notifyAll()方法以一种非常低级的方式解决任务互操作的,即每次交互时都要握手。在许多情况下,我们使用更高的抽象级别,使用同步队列来解决任务协作问题,同步队列在许多情况下只允许一个任务插入或删除元素。在java.util.concurrent.BlockingQueue接口中提供了这个队列.我们常用的有:

  • LinkedBlockingQueue它是一个无界队列
  • ArrayBlockingQueue它具有固定尺寸,可以在其被阻塞之前,向其中放置固定数量的元素。

如果消费者任务试图从队列中获取对象,而队列此时为空,那么这些队列还可以挂起消费者任务,并且当有更多的元素可用时恢复消费者任务。阻塞队列可以解决非常大量的问题,其方式与wait()和notifyAll()相比,则简单可靠的多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值