java编程思想-多线程总结(三)

三、终结任务

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();
    }
}

<setup condition for T2>是防止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)必须有循环等待,这时一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直等待下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容