wait-notify 与 保护性暂停设计

wait-notify

原理简单介绍

简单认识下图:

  • Monitor 监视器,其实就是synchronized锁的那个对象,在Java中,每个对象都可以关联一个Monitor对象,其实例存储在堆中
  • Thread-2 某个线程,现在是Monitor的Owner,也就是获得了当前锁
  • EntryList 里面是未竞争到锁而阻塞等待的线程
  • WaitSet 先前竞争到的线程,但因为没有足够资源需要wait的 

  • OWNER 线程发现条件不满足,调用wait方法,即可进入WaitSet变为Waiting状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用CUP时间片
  • BLOCKED 线程会在 OWNER 线程释放锁时被唤醒
  • WAITING 线程会在OWNER 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味着立刻获取锁,仍需进入EntryList重新竞争

基本API

  • obj.wait() 让进入Object监视器的线程到 waitSet等待

  • obj.wait(long timeout) 有时限的等待, 到n毫秒后结束等待,或是被唤醒

  • obj.notify() 在 object 上正在 waitSet 等待的线程中随机挑一个唤醒

  • obj.ontifyAll() 让object上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法。

需要获取对象锁后才可以调用 锁对象.wait()(成为锁的Owner才能wait),notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU

说明:wait 是挂起线程,需要唤醒的都是挂起操作,阻塞线程可以自己去争抢锁,挂起的线程需要唤醒后去争抢锁

对比 sleep():

  • 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信

  • 锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU

  • 使用区域不同:wait() 方法必须放在同步控制方法和同步代码块(先获取锁)中使用,sleep() 方法则可以放在任何地方使用

同步模式之保护性暂停

定义

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果

要点

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

单任务版:

GuardedObject类:

class GuardedObject {
    // 结果对象
    private Object response;
    // 锁对象
    private final Object lock = new Object();

    //获取结果
    //timeout :最大等待时间
    /**
    * get方法,利用wait的原理,在获取锁后即进入等待    
    */
    public Object get(long millis) {
        synchronized (lock) {
            // 1) 记录最初时间
            long begin = System.currentTimeMillis();
            // 2) 已经经历的时间
            long timePassed = 0;
            // 一直循环,直至response不为空或等待时间millis到了
            while (response == null) {
                // 4) 假设 millis 是 1000,结果在 400 时虚假唤醒了,那么还有 600 要等
                long waitTime = millis - timePassed;
                log.debug("waitTime: {}", waitTime);
                //经历时间超过最大等待时间退出循环
                if (waitTime <= 0) {
                    log.debug("break...");
                    break;
                }
                try {
                    // 当前线程进入waitSet等待,等待唤醒
                    lock.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 3) 如果提前被唤醒,这时已经经历的时间假设为 400
                // 这里每次while循环的pass时间是要记录的,要考虑虚假唤醒的情况,如果某个线程一直被虚假唤醒,如果不给mills等待时间减去pass时间,就会一直等待下去
                timePassed = System.currentTimeMillis() - begin;
                log.debug("timePassed: {}, object is null {}",
                        timePassed, response == null);
            }
            return response;
        }
    }

    //产生结果
    / **
    * 生产结果对象的方法,每次生产出一个结果,就唤醒waitSet中所有的等待线程
    */
    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足,通知等待线程
            this.response = response;
            log.debug("notify...");
            lock.notifyAll();
        }
    }
}

测试:

public static void main(String[] args) {
    GuardedObject object = new GuardedObject();
    new Thread(() -> {
        sleep(1);
        object.complete(Arrays.asList("a", "b", "c"));
    }).start();
    
    Object response = object.get(2500);
    if (response != null) {
        log.debug("get response: [{}] lines", ((List<String>) response).size());
    } else {
        log.debug("can't get response");
    }
}

多任务版:

我们以 收件(消费需求 Peolel类)和  派件(生产需求 Postman类)  为例

GuardedObject:

虽然乍一看,保护性暂停的多任务模式很像mq消息队列,但是保护性暂停要求消费和生产必须一一对应不能复用。

所以我们需要为GuardedObject类加上一个用于唯一标识的成员变量,其他与单任务版是一致的

class GuardedObject {
    //标识,Guarded Object
    private int id;//添加get set方法

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public GuardedObject(int id) {
        this.id = id;
    }

    // 要返回的结果对象
    private Object response;
    // 锁对象
    private final Object lock = new Object();

    //获取结果
    //timeout :最大等待时间
    public Object get(long millis) {
        synchronized (lock) {
            // 1) 记录最初时间
            long begin = System.currentTimeMillis();
            // 2) 已经经历的时间
            long timePassed = 0;
            // 如果没有对应的结果就继续循环
            while (response == null) {
                // 4) 假设 millis 是 1000,结果在 400 时虚假唤醒了,那么还有 600 要等
                long waitTime = millis - timePassed;
                log.debug("waitTime: {}", waitTime);
                //经历时间超过最大等待时间退出循环
                if (waitTime <= 0) {
                    log.debug("break...");
                    break;
                }
                try {
                    lock.wait(waitTime); // 线程进入waitSet阻塞等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 3) 如果提前被唤醒(真正的唤醒或者虚假唤醒),这时已经经历的时间假设为 400
                // 一定要记录下这一段while循环所等待的时间,否则如果某个wait线程一直被虚假唤醒,那么等待时间mills就形同虚设了
                timePassed = System.currentTimeMillis() - begin;
                log.debug("timePassed: {}, object is null {}", timePassed, response == null);
            }
            return response;
        }
    }

    //产生结果
    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足,通知等待线程
            this.response = response;
            log.debug("notify...");
            lock.notifyAll(); // 有消息了,唤醒waitSet中所有等待的阻塞队列
        }
    }
}

Mailboxes: 

Mailboxes扮演多任务的一个类似消息中台的角色,它管理所有的GuardedObject

class  Mailboxes {
    // 为了保证线程安全,实现类为HashTable
    private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
    private static int id = 1;

    // 产生唯一的id
    private static synchronized int generateId() {
        return id ++;
    }

    public static GuardedObject getGuardedObject(int id) {
        return boxes.remove(id);
    }

    public static GuardedObject createGuardedObject() {
        GuardedObject go = new GuardedObject(generateId());
        boxes.put(go.getId(), go);
        return go;
    }

    // 用于获取信箱中所有消息的id
    public static Set<Integer> getIds() {
        return boxes.keySet();
    }
}

Peopel

扮演类似消费者的对象,继承Thread类,重写run方法。

/**
 * 收件人(消费者)
 * 继承Thread类并重写run方法
 */
@Slf4j(topic = "c.People")
class People extends Thread{
    @Override
    public void run() {
        // 收信
        // 1) 向Mailboxes申请一个GuardedObject
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
        log.debug("开始收信 id:{}", guardedObject.getId());
        // 2) 线程进入waitSet阻塞等待信件,直到waitTime或有消息出现才重新进入阻塞队列竞争CPU时间片
        Object mail = guardedObject.get(5000);
        log.debug("收到信id:{},内容:{}", guardedObject.getId(),mail);
    }
}

Postman

/**
 * 送件人(生产者)
 * 在保护性暂停设计模式下,一个Peopel必定对应一个Postman
 * 继承Thread类重写run方法
 */
@Slf4j(topic = "c.Postman")
class Postman extends Thread{
    private int id;
    private String mail;

    public Postman(Integer id, String s) {
        this.id = id;
        this.mail = s;
    }

    //构造方法
    @Override
    public void run() {
        // 从Mailboxes中拿到待派件的id对应的GuardedObject
        GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
        log.debug("开始送信i d:{},内容:{}", guardedObject.getId(), mail);
        // 生产消息,complete方法生产消息成功后会唤醒所有waitSet的等待线程
        guardedObject.complete(mail);
    }
}

测试:

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            // 三个收件线程,开启后向MailBoxes提交收件申请(Mailboxes.createGuardedObejt)
            new People().start();
        }
        // 主线程休眠一段时间,保证所有收件线程都能提交收件的申请
        Thread.sleep(1000);
        // 遍历Mailboexs,拿到需要派件的id,派发信件
        for (Integer id : Mailboxes.getIds()) {
            new Postman(id, id + "号快递到了").start();
        }
    }

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值