JUC学习03 共享模型02

本文深入探讨了Java中的wait/notify机制,详细解释了wait、notify和notifyAll方法的使用场景及区别。通过示例展示了如何在多线程环境中实现线程间的协作,包括保护性暂停模式、避免虚假唤醒和实现异步模式的生产者消费者问题。文章还提到了线程的sleep方法与wait方法的区别,强调了在同步控制中的重要性。
摘要由CSDN通过智能技术生成

 wait使用场景

        

        当Owner线程发现条件不满足,就会进入wait状态,进入Monitor中的WaitSet变成WATING状态。在WATING的县城和BLOCKED一样都是阻塞状态,不会占有CPU的时间片。但BLOCKED状态的线程会在Owner线程释放锁时唤醒。而WAITING线程会在Owner调用notify或notifyAll时唤醒,唤醒后也不意味着马上就会获得锁,而是重新进入EntryList进行竞争。

wait/notify

  • wait():使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。
  • wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
  • wait(long,int):对于超时时间更细力度的控制,单位为纳秒。
  • notify():随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
  • notifyAll():使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。
           
  • 被wait的前提是,你获得了Owner,也就是你竞争到了锁。

        

static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{

            synchronized (lock){
                System.out.println("执行t1");
                try {
                    //lock.wait();
                    lock.wait(1000);//这里表示,如果等待一秒后还是没有被notify叫醒,那他就会继续执行下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1:执行其他代码");
            }

        },"t1").start();
        new Thread(()->{
            synchronized (lock){
                System.out.println("执行t2");
                try {
                    lock.wait();//不加参数的话它会一直等待下去,直到notify唤醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2:执行其他代码");
            }
        },"t2").start();
        TimeUnit.SECONDS.sleep(2);//让主线程休息两秒再执行
        System.out.println("唤醒lock上的其它线程");

        synchronized (lock) {
            //lock.notify(); // 唤醒obj上一个线程
            lock.notifyAll(); // 唤醒obj上所有等待线程
        }
    }

wait和sleep的比较

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

从使用角度看,sleep是Thread线程类的方法,而wait是Object顶级类的方法。

sleep可以在任何地方使用,而wait只能在同步方法或者同步块中使用。

CPU及资源锁释放

sleep,wait调用后都会暂停当前线程并让出cpu的执行时间,但不同的是sleep不会释放当前持有的对象的锁资源,到时间后会继续执行,而wait会放弃所有锁并需要notify/notifyAll后重新获取到对象锁资源后才能继续执行。

sleep和wait的区别:

  • 1、sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
  • 2、sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
  • 3、它们都可以被interrupted方法中断。

具体来说:

Thread.Sleep(1000) 意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。另外值得一提的是Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。
 

wait/notify的使用姿势

        step1

        

@Slf4j(topic = "c.Test01")
public class Test01 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            // 这里能不能加 synchronized (room)?
            hasCigarette = true;
            log.debug("烟到了噢!");
        }, "送烟的").start();
    }

 送烟的线程能不能加synchronized (room)?

        不能,因为此时小南占据了room,它只是调用了sleep。并不会放弃锁。如果送烟线程也用了锁,那送烟线程得等到小南线程执行结束以后才可以获得锁并送烟。

        其实这个线程使用sleep也不好,烟1秒后送到,小南却睡了两秒,中间有一秒钟的资源浪费。小南锁住了room,其它人不需要烟也可以干活,但是因为没有锁,导致不能够执行。这样的效率是非常低的。

 改进方法:小南线程里的sleep换成wait,送烟线程加上synchronized锁住room并调用notify方法。

step3

static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小南").start();
        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notify();
            }
        }, "送外卖的").start();
    }

运行结果:

        notify这时候就出现问题了,因为notify是随机叫醒一个线程,此时外卖线程里调用了notify,但是却叫醒了小南线程,产生了虚假唤醒的现象。 

        解决方法就是直接用notifyAll。

        其实面对小南被无辜唤醒的事,可以通过把小南线程里的if判断换成while循环来解决。

保护性暂停

        

保护性暂停,即Guarded Suspension,用一个线程等待另一个线程的执行结果

要点:

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

@Slf4j(topic = "c.GuardedSuspensionTest")
public class GuardedSuspensionTest {
    public static void main(String[] args) {
        //模拟线程1等待线程2的结果
        GuardedObject guardedObject = new GuardedObject();
        new Thread(()->{
            //等待结果
            try {
                log.info("线程一等待结果中.....");
                LinkedList list = (LinkedList) guardedObject.get();
                log.debug("结果返回了{}",list.get(0));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1").start();
        new Thread(()->{
            //等待结果
            log.info("线程二执行业务逻辑中.....");
            List<String> list = new LinkedList<>();
            for (int i = 65; i < 65+26; i++) {
                list.add(String.valueOf((char) i));
            }
            guardedObject.complete(list);
        },"t2").start();
    }
}
class GuardedObject{
    //结果
    private Object response;
    //获取结果的方法
    public synchronized Object get() throws InterruptedException {
        while (response == null){
            //表示还没有结果,一直等待下去。
            this.wait();
        }
        //跳出循环后就可以返回不为空的response了
        return this.response;
    }
    //产生结果
    public synchronized void complete(Object response){
        //给response赋值
        this.response = response;
        this.notifyAll();
    }
}

 保护性暂停模式可以避免使用join,并且可以避免使用全局变量。

扩展:增加超时

线程一只等待一个最大时间,超时的话立马结束while循环。

@Slf4j(topic = "c.GuardedSuspensionTest")
public class GuardedSuspensionTest {
    public static void main(String[] args) {
        //模拟线程1等待线程2的结果
        GuardedObject guardedObject = new GuardedObject();
        new Thread(()->{
            //等待结果
            try {
                log.info("线程一等待结果中.....");
                LinkedList list = (LinkedList) guardedObject.get(2000);
                log.debug("结果返回了{}",list.get(0));
            } catch (Exception e) {
                //e.printStackTrace();
                log.debug("俺不等了!!!");
            }
        },"t1").start();
        new Thread(()->{
            //等待结果
            log.info("线程二执行业务逻辑中.....");
            List<String> list = new LinkedList<>();
            for (int i = 65; i < 65+26; i++) {
                list.add(String.valueOf((char) i));
            }
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            guardedObject.complete(list);
        },"t2").start();
    }
}
class GuardedObject{
    //结果
    private Object response;
    //获取结果的方法
    public synchronized Object get(long timeout) throws InterruptedException {
        //开始时间
        long begin = System.currentTimeMillis();
        //已经过去的时间
        long passMillis = 0;
        while (response == null){
            //破除循环
            if(passMillis >= timeout){
                break;
            }
            //表示还没有结果,一直等待下去。
            this.wait(timeout-passMillis); //为了防止虚假唤醒,所以每次等待的时间应该是timeout-passMillis
            //在这里记录一个已经过去的时间,如果已经过去了,那就不让这个循环继续下去。
            passMillis = System.currentTimeMillis() - begin;
        }
        //跳出循环后就可以返回不为空的response了
        return this.response;
    }
    //产生结果
    public synchronized void complete(Object response){
        //给response赋值
        this.response = response;
        this.notifyAll();
    }
}

  为什么要wait(timeout-passMillis)呢,这是因为如果第一次循环,并不满足条件,那wait实际上第一次是wait了2s,假设1s后进入了第二次循环,这时不满足破除循环的条件,但wait还需要再wait两秒吗?显然是不需要的,而是只需要wait1s即可,我们已经记录了已经过去的时间,那只要求出还需要最多等多久的时间就够了,所以要wait的参数要填wait(timeout-passMillis)。

Join原理

        下面是Join的源码

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

        join的底层也是使用wait来实现的,wait方法也是调用了wait(0)来实现。wait(0)就是一直让线程等待下去,join方法本质就是利用上面的线程实例作为对象锁的原理,当线程终止时,会调用线程自身的notifyAll()方法,通知所有等待在该线程对象上的线程的特征。

        如果参数大于0的话,也就相当于刚才写的代码。通过记录开始时间和经过时间,来确定需要wait多久来保证wait时间是符合参数的。也就是保护性暂停模式。

扩展2:解耦

        多任务版 GuardedObject图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。和生产者消费者模式的区别就是:这个生产者和消费者之间是一一对应的关系,但是生产者消费者模式并不是。rpc框架的调用中就使用到了这种模式。

@Slf4j(topic = "c.GuardedSuspensionTest")
public class GuardedSuspensionTest {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new People().start();
        }
        TimeUnit.SECONDS.sleep(1);

        for (Integer i: MailBoxes.getIds()) {
            new PostMan(i,"恭喜收到信,内容ID:"+i).start();
        }
    }
}
//因为是做的多线程,所以直接用继承Thread类的方式来实现。
@Slf4j(topic = "c.People")
class People extends Thread{
    //重写run方法。居民等待信件到来。
    @Override
    public void run() {
        GuardedObject guardedObject = MailBoxes.createGuardedObject();
        log.debug("开始收信........");
        try {
            Object mail = guardedObject.get(5000);
            log.debug("收到id为{}的信成功,结果是{}",guardedObject.getId(),mail.toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.debug("收信失败了........");
        }

    }
}
@Slf4j(topic = "c.PostMan")
class PostMan extends Thread{
    //因为邮递员比较特殊,他是生产者,所以他需要两个信息,邮箱ID和信件内容(笑嘻了
    private int mailId;
    private String mail;
    public PostMan(int id,String mail){
        this.mailId = id;
        this.mail = mail+mailId;
    }
    @Override
    public void run() {
       GuardedObject guardedObject = MailBoxes.getGuardedObject(mailId);
       log.debug("送信咯:{}",mail);
       guardedObject.complete(mail);
    }
}
class MailBoxes{
    private static Map<Integer,GuardedObject> boxes = new Hashtable<>();//这里暂时使用hashtable

    private static int id = 1;
    //给GuardedObject产生一个唯一ID
    private static synchronized int generatorId(){
        //毕竟是多线程操作所以必须加上synchronized,而且要记住synchronized加在静态方法下就是给MailBoxes类对象加锁!!!
        return id++;
    }
    public static GuardedObject createGuardedObject(){
        GuardedObject go = new GuardedObject(generatorId());
        boxes.put(go.getId(),go);
        return go;
    }
    public static Set<Integer> getIds(){
        return boxes.keySet();
    }
    public static GuardedObject getGuardedObject(int id){
        //从优雅性上考虑,反正居民不会收到第二次信,那我们就在获取到实例后直接把它从Map中删除。
        return boxes.remove(id);
    }
}
class GuardedObject{
    //用来区分其它实例
    private int id;
    //结果
    private Object response;

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

    public int getId() {
        return id;
    }

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

    //获取结果的方法
    public synchronized Object get(long timeout) throws InterruptedException {
        //开始时间
        long begin = System.currentTimeMillis();
        //已经过去的时间
        long passMillis = 0;
        while (response == null){
            //破除循环
            if(passMillis >= timeout){
                break;
            }
            //表示还没有结果,一直等待下去。
            this.wait(timeout-passMillis); //为了防止虚假唤醒,所以每次等待的时间应该是timeout-passMillis
            //在这里记录一个已经过去的时间,如果已经过去了,那就不让这个循环继续下去。
            passMillis = System.currentTimeMillis() - begin;
        }
        //跳出循环后就可以返回不为空的response了
        return this.response;
    }
    //产生结果
    public synchronized void complete(Object response){
        //给response赋值
        this.response = response;
        this.notifyAll();
    }
}

算是一次应用吧。

异步模式之生产者消费者

要点

  1. 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  2. 消费队列可以用来平衡生产和消费的线程资源
  3. 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  4. 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  5. JDK 中各种阻塞队列,采用的就是这种模式

“异步”的意思就是生产者产生消息之后消息没有被立刻消费,而“同步模式”中,消息在产生之后被立刻消费了。

1594524622020

我们写一个线程间通信的消息队列,要注意区别,像rabbit mq等消息框架是进程间通信的。

@Slf4j(topic = "c.Test02")
public class Test02 {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);

        for (int i = 0; i < 3; i++) {
            int id = i;
            new Thread(() -> {
                try {
                    queue.put(new Message(id, "balabala" + id));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "生产者" + i).start();
        }

        new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                    queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者").start();
    }
}

//消息队列类,仅用来线程通信
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
    //消息的队列集合
    private LinkedList<Message> channel = new LinkedList<>();
    //容器容量
    private int capCity;

    public MessageQueue(int capCity) {
        this.capCity = capCity;
    }

    //获取消息的方法
    public Message take() throws InterruptedException {
        //检查队列是否为空,为空的话消费者就进入等待。
        synchronized (channel) {
            while (channel.isEmpty()) {
                log.debug("队列为空");
                channel.wait();
            }

            //返回第一条消息并将消息从队列中剔除。
            Message message = channel.removeFirst();
            log.debug("取出消息:{}", message.getValue());
            //通知生产者线程可以接着放入了
            channel.notifyAll();
            return message;
        }

    }

    //存入消息的方法
    public void put(Message message) throws InterruptedException {
        synchronized (channel) {
            //检查队列是否满了,满了的话则进入等待
            while (channel.size() == capCity) {
                log.debug("队列已满");
                channel.wait();
            }
            //将消息加入队列的尾部
            channel.addLast(message);
            log.debug("生产消息:{}", message.getValue());
            //通知消费者线程可以消费了
            channel.notifyAll();
        }
    }
}

//因为消息的唯一性很重要,所以我们自己建一个Message类
final class Message {
    private int id;
    private Object value;

    public Message(int id, Object value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

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

    public Object getValue() {
        return value;
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值