Java线程的挂起与恢复 wait(), notify()方法介绍

一, 什么是线程的挂起与恢复

从字面理解也很简单.

所谓线程挂起就是指暂停线程的执行(阻塞状态).

而恢复时就是让暂停的线程得以继续执行.(返回就绪状态)


二, 为何需要挂起和恢复线程.

我们来看1个经典的例子(生产消费):

1个仓库最多容纳6个产品, 制造者现在需要制造超过20件产品存入仓库, 销售者要从仓库取出这20件产品来消费.

制造和消费的速度很可能是不一样的, 编程实现两者的同步.


我们来简单分析一下.要把这个问题转化为编程模型. 无非几点.

1. 把仓库设为1个容器, 容量为6

2. 把生产 和 消费设为两线程.

3. 无论生产还是消费, 每次的个数都是1.

4. 生产线程往仓库添加20个产品, 消费线程从仓库取20个产品.

5. 仓库满时, 生产线程必须暂停, 仓库空时, 消费必须暂停.


可见, 为了满足第5个条件, 线程必须有暂停和恢复执行的功能. 这就是需要挂起和恢复线程的原因.

其实这个问题还有1个隐藏条件:

因为有2个线程同时访问修改同1个数据(容器), 所以生产和消费线程的关键代码必须是互斥的.

亦即系讲, 当生产线程访问和修改容器时, 恢复线程就必须阻塞, 否则数据会出错.



三, 生产消费问题的简单代码(不考虑挂起恢复)

我们一步一步利用java代码来实现这个问题.

3.1 产品

我们只需要定义1个类来描述产品.

本例中的产品只有1个属性id (int 类型)用于区分.

代码:

class Prod_1{
    private int id;
    public int getId(){
        return this.id;
    }
    public Prod_1(int id){
        this.id = id;
    }
}


3.2 仓库(容器)

上面说过了, 仓库的数据模型,实际上就是1个容器.

编程中常用的容器无非是栈和队列. (每次进出的个数都只能是1)

而在仓库管理中, 一般会优先把生产日期早的产品出库(先进先出), 所以这里就用队列来实现了


例子中的队列是1个静态(数组)队列.

静态队列的代码实现其实并不容易理解. 如果有兴趣的话可以看看本人关于静态队列的介绍: http://blog.csdn.net/nvd11/article/details/8816699

但在这里, 只需要明白两个方法作用就ok了

1. public synchronized void enqueue(object ob)

作用是把ob放入队列中, 也就是入库.


2. public synchronized ob deQueue()

把队列中最早放入的元素(返回值)拿出来, 也就是出库.


注意上面两个方法是同步的, 也就是互斥的.


代码:

class ProdQueue_1{
    private Prod_1[] prod_q;
    private int pRear;
    private int pFront;

    public ProdQueue_1(int len){
        prod_q = new Prod_1[len + 1]; //array queue. set the max length = capacity + 1
        pRear = 0;
        pFront = 0;
    }

    public int getLen(){
        return (pRear - pFront + prod_q.length) % prod_q.length;
    }
    
    public boolean isEmpty(){
        return (pRear == pFront);
    }

    public boolean isFull(){
        return ((pRear + 1) % prod_q.length == pFront); 
    } 

    public synchronized void enQueue(Prod_1 p){
        if (this.isFull()){
            throw new RuntimeException("the warehouse is full!");
        }

        prod_q[pRear] = p;
        pRear = ((pRear + 1) + prod_q.length) % prod_q.length;
    } 

    public synchronized Prod_1 deQueue(){
        if (this.isEmpty()){
            throw new RuntimeException("the warehouse is empty!");
        }  
=
        int p = pFront;
        pFront = ((pFront + 1) + prod_q.length) % prod_q.length; 
        return prod_q[p];
    } 

    public String toString(){
        if (this.isEmpty()){
            return "warehose: empty!";
        }
        StringBuffer s = new StringBuffer("count is " + this.getLen() + ": ");  
        int i;
        for (i=pFront; i != pRear; ){
            s = s.append(prod_q[i].getId() + ",");
            i = ((i+1) + prod_q.length) % prod_q.length;
        }
        s = s.delete(s.length() - 1,s.length());
        return s.toString();
    } 
}

3.3 生产线程

生产线程无非就是不断调用容器类的入列函数(enQueue),把产品不断放入容器中.

当然要接受上面容器类作为1个成员.

而且因为是线程类, 必须实现Runnable接口.


代码如下:

class Producer_1 implements Runnable{
    private ProdQueue_1 pq;
    private int count;
    public Producer_1(ProdQueue_1 pq, int count){
        this.pq = pq;
        this.count = count;
    }

    private void thrdSleep(int ms){
        try{
             Thread.sleep(ms);
        }catch(Exception e){

        }
    }

    public void run(){
        Prod_1 p;
        int i;
        for (i=0; i<count; i++){
            this.thrdSleep(1000);
            p = new Prod_1(i);
            pq.enQueue(p);
            System.out.printf("Producer: made the product %d\n", p.getId());
        }
    }
}

上面就是生产者的类, 构造方法中有两个参数, 分别对应其两个成员:

1个就是容器的对象,  另1个就是要生产的产品数量.

注意, 我在run()方法的循环中加了1个sleep()方法, 代表每生产1个产品停顿1秒(设置生产速度)


3.4 销售线程

销售线程的业务就是不断地从容器中取出产品. 就是执行容器对象的deQueue()方法了.

具体实现方法跟生产线程是类似的. 代码如下:

class Seller_1 implements Runnable{
    private ProdQueue_1 pq;
    public Seller_1(ProdQueue_1 pq){
        this.pq = pq;
    }

    private void thrdSleep(int ms){
        try{
             Thread.sleep(ms);
        }catch(Exception e){

        }
    }

    public void run(){
        Prod_1 p;
        while(true){
            this.thrdSleep(2000);
            p = pq.deQueue();
            System.out.printf("Seller: sold the product %d\n", p.getId());
        }
    }

}

在销售线程中, 每个循环利用sleep()方法停顿2秒, 也就设置了销售速度是比生产速度慢一倍的.


3.5 启动类

国际惯例, 在1个启动类的静态方法中,调用上面写的业务类.

public class Td_prod_1{
    public static void f(){
        ProdQueue_1 pq = new ProdQueue_1(6);
        Producer_1 producer = new Producer_1(pq,20);
        Seller_1 seller = new Seller_1(pq);

        Thread thrd_prod = new Thread(producer);
        thrd_prod.start();

        Thread thrd_sell = new Thread(seller);
        thrd_sell.start();


    }
}

逻辑很简单, 无非是定义1个容器对象.

然后利用这个容器对象构造1个生产线程对象和1个销售线程对象.

最后启动这个两个线程.


3.6 执行结果

执行结果如下:

Producer: made the product 0
Seller: sold the product 0
Producer: made the product 1
Producer: made the product 2
Seller: sold the product 1
Producer: made the product 3
Producer: made the product 4
Seller: sold the product 2
Producer: made the product 5
Producer: made the product 6
Seller: sold the product 3
Producer: made the product 7
Producer: made the product 8
Seller: sold the product 4
Producer: made the product 9
Producer: made the product 10
Seller: sold the product 5
Producer: made the product 11
Exception in thread "Thread-0" java.lang.RuntimeException: the warehouse is full!
	at Thread_kng.Td_wait_notify.ProdQueue_1.enQueue(Td_prod_1.java:38)
	at Thread_kng.Td_wait_notify.Producer_1.run(Td_prod_1.java:92)
	at java.lang.Thread.run(Thread.java:722)
Seller: sold the product 6
Seller: sold the product 7
Seller: sold the product 8
Seller: sold the product 9
Seller: sold the product 10
Seller: sold the product 11
Exception in thread "Thread-1" java.lang.RuntimeException: the warehouse is empty!
	at Thread_kng.Td_wait_notify.ProdQueue_1.deQueue(Td_prod_1.java:47)
	at Thread_kng.Td_wait_notify.Seller_1.run(Td_prod_1.java:118)
	at java.lang.Thread.run(Thread.java:722)

可见到 结果中:

1开始, 生产线程和销售线程是正常执行的, 因为速度的不同, 大概生产线程每生产两个, 销售线程才销售1个.

然后生产线程在生产完第11个产品, 尝试生产产品12时抛异常被中断了. 因为销售线程才销售处第5个.  这时容器有6~11, 满了, 爆仓..

接下来只有1个销售线程执行, 但是销售完容器里面的产品后也抛异常了..  因为仓库已经没有产品.




四, 线程的暂停 wait()

上面程序的销售和执行方法(enQueue 和 deQueue) 是同步的, 但是仍然会出错.

1. 生产和销售速度不一致.

2. 容器容量有限制.


所以必须对容器的入列和出列方法增加1个些处理.


其实, 上面还是做了1写处理的. 这个处理就是令它抛出异常..

如enQueue里面的.

if (this.isFull()){
            throw new RuntimeException("the warehouse is full!");
}

意思就是容器满了, 就抛异常中断线程.


而现实中, 我们应该这样处理: 

如果容器满了, 应该把生产线程暂停.

如果容器空了, 应该把销售线程暂停.


4.1 sleep()方法并不适用

如果我们用sleep()方法来暂停一个线程是否可行呢? 例如

if (this.isFull()){
            Thread.sleep(10000)
}

sleep方法必须制定暂停的秒数, 而在生产环境中, 我们通常无法判断具体需要暂停多久的.

实际上, 在上面例子中, 我们需要生产线程暂停,直至容器不再为空.

那么容器什么时候不再为空呢, 取决于消费线程.   

而生产环境中,消费线程的消费速度不是确定的.  所以这里sleep()方法不适用于生产销售问题.


4.2 wait()方法介绍

通常我们用wait()方法来暂停1个线程.  首先看看jdk api 对wait()函数的介绍:

public final void wait()
                throws InterruptedException
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待


首先, wait()是基类Object的方法. 需要由1个实例化的对象来调用.

通常, 这个调用wait()方法的对象不应该是线程对象, 而是线程锁定的资源对应的对象.

注意wait() 类似 sleep()会抛出异常, 必须手动catch.


例如1个线程里的run()函数.

public void run(){
   synchronized(a){
      xxxxxx();
   }
}

它为了与其他线程互斥, 锁定了对象a. 

如果在xxxxx()方法中执行了 a.wait() 则导致该线程暂停. 而且释放该线程对资源a的锁定


4.3 为生产销售例子添加wait()方法.

实际上, 我们只需要修改容器类ProdQueue_1就ok了. 在enQueue() 和 deQueue()方法中都添加暂停的逻辑:

    public synchronized void enQueue(Prod_1 p){
        while (this.isFull()){
            try{
                this.wait();
            }catch(Exception e){

            }
        }

        prod_q[pRear] = p;
        pRear = ((pRear + 1) + prod_q.length) % prod_q.length;
    } 

    public synchronized Prod_1 deQueue(){
        while (this.isEmpty()){
            try{
                this.wait();
            }catch(Exception e){

            }
        }  

        int p = pFront;
        pFront = ((pFront + 1) + prod_q.length) % prod_q.length; 
        return prod_q[p];
    } 

上面我使用while来判断 容器的状态(满or空), 而不是用if.

原因, 就是如果用while的话, 一旦被唤醒, 还会返回再检查一次容器状态.  而如果利用if一旦被唤醒,就直接执行下面的代码.

理论上, 用while是更加安全的.


这样的话, 在生产线程中, 入列方法首先会判断队列容器是否已经满, 如果是满的, 就会暂停线程, 并释放锁定的资源.

同样, 在销售线程中, 出列方法会首先判断队列容器是否为空, 如果是空的, 则暂停线程, 释放资源.


注意, 这个例子中的sychronized 关键字是用来修饰方法名的. 也就是锁定的资源是调用方法的对象本身, 也就是this了.

所以是执行this.wait()来暂停线程.



4.4 输出结果

添加了wait()方法后, 输出结果如下:

Producer: made the product 0
Seller: sold the product 0
Producer: made the product 1
Producer: made the product 2
Seller: sold the product 1
Producer: made the product 3
Producer: made the product 4
Seller: sold the product 2
Producer: made the product 5
Producer: made the product 6
Seller: sold the product 3
Producer: made the product 7
Producer: made the product 8
Seller: sold the product 4
Producer: made the product 9
Producer: made the product 10
Seller: sold the product 5
Producer: made the product 11
Seller: sold the product 6
Seller: sold the product 7
Seller: sold the product 8
Seller: sold the product 9
Seller: sold the product 10
Seller: sold the product 11

可以看出, 需要没有抛出异常, 但是实际效果仍然跟上次相似.

当生产线程入列第11个产品后, 尝试入列第12个时, 这时容器满了, 生产线程被暂停.

这时只剩下销售线程在执行, 最终销售完第11个产品时, 容器空了, 销售线程也被暂停.

这时程序只剩下主线程了, 相当于死机状态.

原因是两个业务线程都暂停了,处于等待状态.


这时就需要1个唤醒机制了.


五, 线程的唤醒 notify()

我们首先来看看jdk api 对 notfiy() 方法的介绍.

public final void notify()
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。


我在以前的博文提过了, jdk api中文的翻译水平不是很好.

但是起码要弄明白,  notify()是基类Object的一个非静态方法. 一般是由调用wait()的对象(被锁定的资源)来调用. 

意思就是假如 对象A调用wait() 暂停了线程(A是被锁定的资源对象), 则必须执行A.notfiy()来唤醒.



本屌表达能力也很有限, 还是结合上面例子说明:

但是首先要明白如下几个概念:

5.1 假如A线程被暂停, 那么谁来唤醒A

一个线程被暂停后就不能执行, 所以线程是不能唤醒自己的.

只能由其他线程唤醒.

在这个例子中个, 暂停中的生产线程只能被销售线程唤醒.  同样地, 暂停中的销售线程只能被生产线程唤醒.

当然, 在主线程也可以唤醒它们, 但是不符合业务逻辑.


5.2 什么时候唤醒.

这也是个问题, 在这个问题中, 我们可以这样设置:

当生产线程成功生产1个新产品入列, 这时容器就肯定不为空了, 我们就可以让生产线程尝试唤醒销售线程.

同样, 当销售线程成功出列1个产品时, 这时容器就肯定不是满的, 我们就可以让销售线程尝试唤醒生产线程.

5.3 notify()到底唤醒了哪个线程.

在上面之所以红色高亮了"尝试"这个词, 是因为notify()方法无法唤醒1个指定的线程.

假如有两个线程执行了this.wait()而暂停, 那么在第3个线程中执行this.notfiy()会随机唤醒其中1个.


当然, 这个例子中我们不允许两个线程都被暂停, 所以执行this.notify()就是唤醒对方线程了.

而某一时间, 没有任何线程因为this.wait()而暂停, 那么执行this.notify()则不起任何作用, 但是不会抛出任何异常和报错!


也就说, 当生产线程执行this.notify()时无需事先判断销售线程的状态.  反之亦然.


5.4 修改后的出列和入列方法代码.

既然逻辑理顺了, 那么代码就很简单:

    public synchronized void enQueue(Prod_1 p){
        while (this.isFull()){
            try{
                this.wait();
            }catch(Exception e){

            }
        }

        prod_q[pRear] = p;
        pRear = ((pRear + 1) + prod_q.length) % prod_q.length;
        this.notify();
    } 

    public synchronized Prod_1 deQueue(){
        while (this.isEmpty()){
            try{
                this.wait();
            }catch(Exception e){

            }
        }  

        int p = pFront;
        pFront = ((pFront + 1) + prod_q.length) % prod_q.length; 
        this.notify();
        return prod_q[p];
    } 


逻辑很简单, 无非就是在入列和出列的最后添加this.notify(), 每次成功生产or销售1个产品, 都尝试去唤醒对方线程.

5.6 输出结果

经过这次修改后, 结果如下:

hello ant, it's the my meeting with ant!
Producer: made the product 0
Seller: sold the product 0
Producer: made the product 1
Producer: made the product 2
Seller: sold the product 1
Producer: made the product 3
Producer: made the product 4
Seller: sold the product 2
Producer: made the product 5
Producer: made the product 6
Seller: sold the product 3
Producer: made the product 7
Producer: made the product 8
Seller: sold the product 4
Producer: made the product 9
Producer: made the product 10
Seller: sold the product 5
Producer: made the product 11
Seller: sold the product 6
Producer: made the product 12
Seller: sold the product 7
Producer: made the product 13
Seller: sold the product 8
Producer: made the product 14
Seller: sold the product 9
Producer: made the product 15
Seller: sold the product 10
Producer: made the product 16
Seller: sold the product 11
Producer: made the product 17
Seller: sold the product 12
Producer: made the product 18
Seller: sold the product 13
Producer: made the product 19
Seller: sold the product 14
Seller: sold the product 15
Seller: sold the product 16
Seller: sold the product 17
Seller: sold the product 18
Seller: sold the product 19

可以看出由于速度的不同, 在11个产品前, 生产线程生产两个, 销售线程才销售出1个.

但是在生产11个产品时, 容器满了, 生产线程被暂停.

然后销售线程销售出第6个产品时, 唤醒了生产线程.

生产线程生产出第12个产品, 这时又满了, 再次暂停...

所以后面就是生产和销售线程交替1个1个地生产和销售....



从这个例子中看出在后面的处理似乎体现不出生产线程的速度优势,  但是在实际项目中, 生产和销售的速度并不是固定的.

这个方法其实是相对合理的方法, 解决了本文开始的那个问题.



六, 唤醒所有线程 notifyAll()

notifyAll()也不难理解.

假如上面的题目修改一下, 有两条生产线程和两条销售线程 共4个线程共享1个队列容器.

那么同一时间可能有多条被暂停.


但是notify()方法只会唤醒随机的一条线程.

所以有时就有必要用notifyAll()来唤醒所有暂停中的线程了!



七, suspend() 和 resume()

这个两个方法是类Thread 的非静态方法.

用这个两个方法也可以实现线程的挂起和恢复, 但是suspend()挂起时并不释放被锁定的资源, 容易造成死锁,  JDK API中明确表明不建议使用这个两个方法!


































  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nvd11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值