java并发(三)——wait()、notify()、notifyAll()、join()

锁池和等待池

wait() ,notifyAll(),notify() 三个方法都是Object类中的方法。学习他们之前先要了解俩个概念。java中,每个对象都有两个池,锁(monitor)池和等待池。

锁池: 假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池。

notify()、notifyAll()、wait()

notify(): 通知一个在对象上等待的线程,由WAITING状态变为BLOCKING状态,从等待队列移动到同步队列,等待CPU调度获取该对象的锁,当该线程获取到了对象的锁后,该线程从wait()方法返回。

notifyAll(): 通知所有等待在该对象上的线程,由WAITING状态变为BLOCKING状态,等待CPU调度获取该对象的锁。

wait(): 调用该方法的线程进入WAITING状态,并将当前线程放置到对象的等待队列,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁。

wait(long): 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
wait(long,int): 对于超时时间更细力度的控制,可以达到纳秒。

简单的例子:

等待通知机制,是指一个线程A调用了对象lock的wait()方法进入等待状态,而另一个线程B调用了对象lock的notify()或者notifyAll()方法,线程A收到通知后从对象lock的wait()方法返回,进而执行后续操作。上述两个线程通过对象lock来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

public class WaitNotify{
    //不需要为volatile,因为对于flag的操作均在synchronized锁的保护下进行,可以保证flag的内存可见性
    static boolean flag = true;
    static Object lock = new Object();

    public static void main(String args[]) throws Exception{
        Thread waitThread = new Thread(new Wait(),"WaitThread");
        waitThread.start();
        TimeUnit.SECONDS.sleep(1);//1 second  -> package java.util.cocurrent
        Thread notifyThread = new Thread(new Notify(),"NotifyThread");
        notifyThread.start();
    }

    static class Wait implements Runnable{
        public void run(){
            //加锁,拥有lock的monitor
            synchronized(lock){
                //当条件不满足时,继续wait,同时释放了lock的锁
                while(flag){
                    try{
                        System.out.println("flag is ture, Wait");
                        lock.wait();
                    }catch(InterruptedException e){
                        //除了notify通知,带超时的wait()方法、线程中断机制也能唤醒此线程
                    }
                }
                System.out.println("flag is false,complete");
            }
        }
    }

    static class Notify implements Runnable{
        public void run(){
            synchronized(lock){
                //获取lock的锁,然后进行通知,通知时不会释放lock的锁,直到当前线程释放了lock,调用了notifyAll,并且WaitThread获得了锁之后,wait线程才能从wait()方法返回
                System.out.println("Notify get lock ,begin notify");
                lock.notifyAll();
                flag = false;
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            synchronized(lock){
                System.out.println("Notify get lock again");
            }
        }
    }
}

注意:

  1. wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
  2. wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
  3. notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程 。
  4. wait() 需要被try catch包围,中断也可以使wait等待的线程唤醒。
  5. notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
  6. notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。
  7. 多线程中要测试某个条件的变化,要使用while。只有当前值满足需要值的时候,线程才可以往下执行,所以,必须使用while 循环阻塞。注意,wait() 当被唤醒时候,只是让while循环继续往下走.如果此处用if的话,意味着if继续往下走,会跳出if语句块。但是,notifyAll 只是负责唤醒线程,并不保证条件,所以需要手动来保证程序的逻辑。
  8. wait方法释放锁,notify不释放锁。
  9. 永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。
生产者和消费者

什么是生产者-消费者问题呢?假设有一个公共的容量有限的池子,有两种人,一种是生产者,另一种是消费者。需要满足如下条件:
1、生产者产生资源往池子里添加,前提是池子没有满,如果池子满了,则生产者暂停生产,直到自己的生成能放下池子。
2、消费者消耗池子里的资源,前提是池子的资源不为空,否则消费者暂停消耗,进入等待直到池子里有资源数满足自己的需求。

功能实现

  • 共享仓库接口
public interface AbstractStorage {
    void consume(int num);
    void produce(int num);
}
  • 共享仓库对象
public class Storage1 implements AbstractStorage {
    //仓库最大容量
    private final int MAX_SIZE = 100;
    //仓库存储的载体
    private LinkedList list = new LinkedList();

    //生产产品
    public void produce(int num){
        //同步
        synchronized (list){
            //仓库剩余的容量不足以存放即将要生产的数量,暂停生产
            while(list.size()+num > MAX_SIZE){
                System.out.println("【要生产的产品数量】:" + num + "\t【库存量】:"
                        + list.size() + "\t暂时不能执行生产任务!");

                try {
                    //条件不满足,生产阻塞
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            for(int i=0;i<num;i++){
                list.add(new Object());
            }

            System.out.println("【已经生产产品数】:" + num + "\t【现仓储量为】:" + list.size());

            list.notifyAll();
        }
    }

    //消费产品
    public void consume(int num){
        synchronized (list){
            //不满足消费条件
            while(num > list.size()){
                System.out.println("【要消费的产品数量】:" + num + "\t【库存量】:"
                        + list.size() + "\t暂时不能执行生产任务!");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //消费条件满足,开始消费
            for(int i=0;i<num;i++){
                list.remove();
            }

            System.out.println("【已经消费产品数】:" + num + "\t【现仓储量为】:" + list.size());
            list.notifyAll();
        }
    }
}

  • 生成者
public class Producer extends Thread{
    //每次生产的数量
    private int num ;

    //所属的仓库
    public AbstractStorage abstractStorage;

    public Producer(AbstractStorage abstractStorage){
        this.abstractStorage = abstractStorage;
    }

    public void setNum(int num){
        this.num = num;
    }

    // 线程run函数
    @Override
    public void run() {
        produce(num);
    }

    // 调用仓库Storage的生产函数
    public void produce(int num) {
        abstractStorage.produce(num);
    }
}
  • 消费者
public class Consumer extends Thread{
    // 每次消费的产品数量
    private int num;

    // 所在放置的仓库
    private AbstractStorage abstractStorage1;

    // 构造函数,设置仓库
    public Consumer(AbstractStorage abstractStorage1) {
        this.abstractStorage1 = abstractStorage1;
    }

    // 线程run函数
    public void run() {
        consume(num);
    }

    // 调用仓库Storage的生产函数
    public void consume(int num) {
        abstractStorage1.consume(num);
    }

    public void setNum(int num){
        this.num = num;
    }
}

  • 测试类
public class Test{
    public static void main(String[] args) {
        // 仓库对象
        AbstractStorage abstractStorage = new Storage1();

        // 生产者对象
        Producer p1 = new Producer(abstractStorage);
        Producer p2 = new Producer(abstractStorage);
        Producer p3 = new Producer(abstractStorage);
        Producer p4 = new Producer(abstractStorage);
        Producer p5 = new Producer(abstractStorage);
        Producer p6 = new Producer(abstractStorage);
        Producer p7 = new Producer(abstractStorage);

        // 消费者对象
        Consumer c1 = new Consumer(abstractStorage);
        Consumer c2 = new Consumer(abstractStorage);
        Consumer c3 = new Consumer(abstractStorage);

        // 设置生产者产品生产数量
        p1.setNum(10);
        p2.setNum(10);
        p3.setNum(10);
        p4.setNum(10);
        p5.setNum(10);
        p6.setNum(10);
        p7.setNum(80);

        // 设置消费者产品消费数量
        c1.setNum(50);
        c2.setNum(20);
        c3.setNum(30);

        // 线程开始执行
        c1.start();
        c2.start();
        c3.start();

        p1.start();
        p2.start();
        p3.start();
        p4.start();
        p5.start();
        p6.start();
        p7.start();
    }
}
  • 测试结果
【要消费的产品数量】:30	【库存量】:0	暂时不能执行生产任务!
【已经生产产品数】:10	【现仓储量为】:10
【要消费的产品数量】:30	【库存量】:10	暂时不能执行生产任务!
【已经生产产品数】:10	【现仓储量为】:20
【要消费的产品数量】:30	【库存量】:20	暂时不能执行生产任务!
【已经生产产品数】:10	【现仓储量为】:30
【已经消费产品数】:30	【现仓储量为】:0
【要消费的产品数量】:50	【库存量】:0	暂时不能执行生产任务!
【要消费的产品数量】:20	【库存量】:0	暂时不能执行生产任务!
【已经生产产品数】:10	【现仓储量为】:10
【已经生产产品数】:10	【现仓储量为】:20
【已经消费产品数】:20	【现仓储量为】:0
【要消费的产品数量】:50	【库存量】:0	暂时不能执行生产任务!
【已经生产产品数】:10	【现仓储量为】:10
【已经生产产品数】:80	【现仓储量为】:90
【已经消费产品数】:50	【现仓储量为】:40
join()

Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。

例子:

public class JoinTest {
    public static void main(String [] args) throws InterruptedException {
        ThreadJoinTest t1 = new ThreadJoinTest("小明");
        ThreadJoinTest t2 = new ThreadJoinTest("小东");
        t1.start();
        /**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
         程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕
         所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
         */
        t1.join();
        t2.start();
    }

}
class ThreadJoinTest extends Thread{
    public ThreadJoinTest(String name){
        super(name);
    }
    @Override
    public void run(){
        for(int i=0;i<1000;i++){
            System.out.println(this.getName() + ":" + i);
        }
    }
}

上面程序结果是先打印完小明线程,在打印小东线程;

上面注释也大概说明了join方法的作用:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。注意,这里调用的join方法是没有传参的,join方法其实也可以传递一个参数给它的。例如:t1.join(10);:如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。

join方法必须在线程start方法调用之后调用才有意义。这个也很容易理解:如果一个线程都没有start,那它也就无法同步了。

源码:

    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()方法。(注意这个join方法本身就是synchronized),这样当前线程即处于等待状态,必须执行notify()或notifyAll()才能唤醒,但实际工作上执行完run方法后,并不需要执行notify(),但后继代码也会被唤醒并执行了,这是什么原因呢?通过对Jvm natvie的源码分析,我们发现thread执行完成后,cpp的源码中会在thread执行完毕后,会调用exit方法,该方法中原来隐含有调用notify_all(thread)的动作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值