一起学并发编程 - 等待与通知

waitnotifynotifyAll,这些在多线程中被经常用到的保留关键字,在实际开发的时候很多时候却并没有被大家重视,而本文则是对这些关键字的使用进行描述。

存在即合理

在java中,每个对象都有两个池,锁池(monitor)和等待池(waitset),每个对象又都有waitnotifynotifyAll方法,使用它们可以实现线程之间的通信,只是平时用的较少。

  • wait(): 使当前线程处于等待状态,直到另外的线程调用notifynotifyAll将它唤醒
  • notify(): 唤醒该对象监听的其中一个线程(规则取决于JVM厂商,FILO,FIFO,随机…)
  • notifyAll(): 唤醒该对象监听的所有线程

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

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

注意事项

  • 在调用wait(), notify()或notifyAll()的时候,都必须获得某个对象(注意:不是类)的锁
  • 永远在循环(loop)里调用 waitnotify,而不是在 If 语句中
  • 永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException

使用案例 - 生产消费

private static int i = 0;

static void product() {//生产者
    System.out.println("P->" + (++i));
}

static void consumer() {//消费者
    System.out.println("C->" + i);
}

public static void main(String[] args) {
    new Thread(() -> {
        while (true) {
            product();
        }
    }).start();
    new Thread(() -> {
        while (true) {
            consumer();
        }
    }).start();
}

日志
//P->1
//P->2
//P->3
//P->4
//C->1 
日志

分析: 从日志中可以看到数据会出现多次生产或多次消费的问题,因为在线程执行过程中,两个线程缺少协作关系,都是各干各的,T1线程只管生产数据,不管T2线程是否处理了。

改进方案 - 变量消息传递

private static int i = 0;
private static boolean isProduction = true;

static void product() {//生产者
    if (isProduction) {
        System.out.println("P->" + (++i));
        isProduction = false;
    }
}

static void consumer() {//消费者
    if (!isProduction) {
        System.out.println("C->" + i);
        isProduction = true;
    }
}

分析: 这种情况下我们通过维护一个变量的方式,通知对方,但是效率及其差,线程频繁请求与判断大大的浪费了系统资源,虽然满足了当前要求,但并非是可选方案…

改进方案 - wait/notify

上文已经介绍了使用waitnotify的前提了,接下来看案例

private final static byte[] LOCK = new byte[0];//定义一个锁对象
private static boolean isProduction = true;//消息投递
private static int i = 0;//生产的消息

static void product() {
    synchronized (LOCK) {// 必须是在 synchronized中 使用 wait/notify/notifyAll
        try {
            if (isProduction) {//如果标示位为生产状态,则继续生产
                System.out.println("P->" + (++i));
                isProduction = false;
                LOCK.notify();//消费者可以消费了
            } else {
                LOCK.wait();//说明生产出来的数据还未被消费掉
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

static void consumer() {
    try {
        synchronized (LOCK) {
            if (isProduction) {//如果当前还在生产,那么就暂停消费者线程
                LOCK.wait();
            } else {
                System.out.println("C->" + i);
                isProduction = true;
                LOCK.notify();//通知我已经消费完毕了
            }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    new Thread(() -> {
        while (true) {
            product();
        }
    }).start();
    new Thread(() -> {
        while (true) {
            consumer();
        }
    }).start();
}
日志
//P->1
//C->1
//P->2
//C->2
//.....
//P->354217
//C->354217
日志

分析: 一切都是那么完美,在T1线程中,调用LOCK.wait()将当前线程移入等待池中,并交出执行权,锁池中的其他线程去竞争并取得锁的使用权(T2线程获取),当T2线程消费完毕后,调用LOCK.notify()方法通知当前对象锁等待池中的其中一个线程(因为这里notify是基于JVM算法而定,因为我们只有两个线程,所以T1线程会接收到T2线程发出的通知,从而继续生产数据。

问题: 虽然一对一没有问题,但假设多个生产者多个消费者的情况下怎么办呢?

BUG - 埋点

public static void main(String[] args) {
    Stream.of("P1", "P2", "P3", "P4").forEach(name -> new Thread(() -> {
        while (true) {
            product();
        }
    }, name).start());
    Stream.of("C1", "C2").forEach(name -> new Thread(() -> {
        while (true) {
            consumer();
        }
    }, name).start());
}
日志
//P2 -> 1
//C2 -> 1
//P2 -> 2
//C1 -> 2
//P3 -> 3
日志

分析: 居然不执行了,借助前面说过的 死锁分析知识,我们看看是不是发生死锁了

线程等待

结果表明,虽然没有Found one deadlock...字眼,但是可以看到有个线程都被wait住了,没有被释放,所以导致我们当前无法继续生产消费

解决方案 - notifyAll

LOCK.notifyAll();//通知所有线程,我已经消费完毕了,你们继续生产

分析: 这里只修改了一句代码,就是将consumer方法中的notify -> notifyAll,由通知单个线程变成通知所有在等待池中的线程

P1 -> 1
C1 -> 1
P2 -> 2
C2 -> 2
...
P3 -> 42894
C1 -> 42894
...
P1 -> 42898
C1 -> 42898

- 说点什么

全文代码:https://gitee.com/battcn/battcn-concurent/tree/master/Chapter1-1/battcn-thread/src/main/java/com/battcn/chapter7

  • 个人QQ:1837307557
  • battcn开源群(适合新手):391619659

微信公众号:battcn(欢迎调戏)

喜大普奔,迎来了十一国庆节….

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值