java编程思想-java中的并发(三)

僤鯓⒐⒋嵵緔 提交于 2020-01-14 12:13:09

三、终结任务

1. 在阻塞时终结

线程状态

一个线程可以处于以下四种状态之一:

  • 1)新建(new):当线程被创建时,他只会短暂的处于这种状态。此时,他已经分配了必须的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将把这个线程转变为可运行zhuang't状态或阻塞状态。
  • 2)就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,他就可以运行,这不同于死亡和阻塞状态。
  • 3)阻塞(Blocked):线程能够运行,但是某个条件阻止他的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入就绪状态,他才可能执行操作。
  • 4)死亡(Dead):处于死亡或终止状态的线程将是不可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。

进入阻塞状态

一个任务进入阻塞状态,可能有如下原因:

  • 1)通过调用sleep()使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。
  • 2)调用wait()使线程挂起,直到线程得到了notify()或notifyAll()消息(或者concurrent类库中等价的signal()或signalAll()消息),线程才会进入就绪状态。
  • 3)任务在等待某个输入/输出完成。
  • 4)任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。

四、线程之间的协作

当使用线程来同时运行多个任务时,可以通过使用锁(互斥)来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。也就是说,如果两个任务在交替着步入某向资源(通常是内存),你可以使用互斥来使得任何时刻只有一个任务可以访问这项资源。

当任务协作时,关键问题是这些任务之间的握手。为了实现这种握手,我们使用了相同的基础特性:互斥。在这种情况下,互斥能够确保只有一个任务可以响应某个信号,这样就可以根除任何可能的竞争条件。在互斥之上,我们为任务添加了一种途径,可以将其自身挂起,直到某些外部条件发生变化,表示是时候让这个任务向前开动了为止。

1. wait()和notifyAll()

wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。通常,这种条件将由另一个任务来改变。你肯定不想在你的任务测试这个条件的同时,不断地进行空循环,这被称为忙等待,通畅是一种不良的CPU周期使用方式。因此wait()会在等待外部世界产生变化的时候将任务挂起,并且只有在notify()或notifyAll()发生时,即表示发生了某些感兴趣的事物,这个任务才会被唤醒并去检查所产生的变化。因此,wait()提供了一种在任务之间对活动同步的形式。

调用sleep()的时候锁并没有被释放,调用yield()也属于这种情况,理解这一点很重要。另一方面,当一个任务在方法里遇到了对wait()的调用的时候,线程的执行被挂起,对象上的锁被释放。因为wait()将释放锁,这就意味着另一个任务可以获得这个锁,因此在该对象(现在是未锁定的)中的其他synchronized方法可以在wait()期间被调用。这点至关重要。因为这些其他的方法通常将会产生改变,而这种改变正是使被挂起的任务重新唤醒所感兴趣的变化。

有两种形式的wait()。第一种版本接受毫秒数作为参数,含义与sleep()方法里的参数意思相同,都是指“在此期间暂停”。但是与sleep()不同的是,对于wait()而言:

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

第二种,也是更常用形式的wait()不接受任何参数。这种wait()将无限等待下去,直到线程接收到notify()或者notifyAll()消息。

wait()、notify()以及notifyAll()有一个比较特殊的方面,那就是这些方法是基类Object的一部分,而不属于Threadde的一部分。所以你可以把wait()放进任何同步控制方法里,而不用考虑这个类是继承自Thread还是实现了Runnable接口。实际上,只能在同步控制方法或同步控制块li里调用wait()、notify()和notifyAll()(因为不用操作锁,所以sleep()可以在非同步控制方法里调用)。调用wait()、notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁。

可以让另一个对象执行某种操作以维护其自己的锁。要这么做的话,必须首先得到对象的锁。比如,如果要向对象x发送notifyAll(),就必须在能够取得x的锁的同步控制块中这么做:

synchronized(x){
    x.notifyAll();
}

我们看一下示例,WaxOMatic.java有两个过程,一个是将la蜡涂到Car上,一个是抛光它。抛光任务在涂蜡任务完成之前,是不能执行其工作的,而涂蜡任务在涂另一层蜡之前,必须等待抛光任务完成。WaxOn和WaxOff都使用了Car对象,该对象在这些任务等待条件变化的时候,使用wait()和notifyAll()来挂起和重新启动这些任务:

public class WaxOMatic {
    public static void main(String[] args) throws Exception{
        Car car = new Car();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new WaxOff(car));
        exec.execute(new WaxOn(car));
        TimeUnit.SECONDS.sleep(5);
        exec.shutdown();
    }
}

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(200);
                car.waxed();
                car.waitForBuffing();
            }
        }catch (InterruptedException e){
            System.out.println("exiting via interrupt");
        }
        System.out.println("ending wax on task");
    }
}

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){
            System.out.println("exiting via interrupt");
        }
        System.out.println("ending wax off task");
    }
}

错失的信号

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

T1:
synchronized(sharedMonitor){
    <setup condition for T2>
    sharedMonitor.notify();
}

T2:
while(someCondition){
    //Point 1
    synchronized(sharedMonitor){
        sharedMonitor.wait();
    }
}

是防止T2调用wait()的一个动作,当然前提是T2还没有调用wait()。

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

该问题的解决方案是防止在someCondition变量上产生竞争条件。下面是T2正确的执行方式:

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

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

2. 生产者-消费者与队列

wait()和notifyAll()方法是一种非常低级的方式解决了任何互操作问题,即每次交互shi时都握手。在许多情况下,你可以瞄向更高的抽象级别,使用同步队列来解决任务协作问题,同步队列在任何时刻都只允许一个任务插入或移除元素。在java.util.concurrent.BlockingQueue接口中提供了这个队列,这个接口有大量的标准实现。你通常可以使用LinkedBlockingQueue,他是一个无界队列,还可以使用ArrayBlockingQueue,它具有固定的尺寸,因此你可以在他被阻塞之前,向其中放置有限数量的元素。

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

3. 死锁

任务可以变成阻塞状态,所以就可能出现这种情况:某个任务在等待另一个任务,而后者又等待别的任务,这样一直下去,直到这条链条上的任务又在等待第一个任务释放锁。这得到了一个任务之间等待的连续循环,没有哪个线程能继续,这被称为死锁。

要修正死锁问题,你必须明白,当以下四个条件同时满足时,就会发生死锁:

  • 1)互斥条件。任务使用的资源中至少有一个是不能共享的。
  • 2)至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。
  • 3)资源不能被任务抢占,任务必须把资源释放当做普通事件。
  • 4)必须有循环等待,这时一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直等待下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。

因为要发生死锁的话,所有这些条件必须全部满足;所以要防止死锁的话,只需破坏其中一个即可。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!