等待唤醒机制和阻塞队列

在这里插入图片描述

 

1. 等待唤醒机制

由于线程的随机调度,可能会出现“线程饿死”的问题:也就是一个线程加锁执行,然后解锁,其他线程抢不到,一直是这个线程在重复操作

void wait()

当前线程等待,直到被其他线程唤醒

void notify()

随机唤醒单个线程

void notifyAll()

唤醒所有线程

等待(wait):当一个线程执行到某个对象的wait()方法时,它会释放当前持有的锁(如果有的话),并进入等待状态。此时,线程不再参与CPU的调度,直到其他线程调用同一对象的notify()或notifyAll()方法将其唤醒,类似的,wait() 方法也可以传入一个参数表示等待的时间,不加参数就会一直等

唤醒(notify/notifyAll):

notify: 唤醒在该对象监视器上等待的某个线程,如果有多个线程在等待,那么具体唤醒哪一个是随机的

notifyAll: 唤醒在该对象监视器上等待的所有线程

1.1. wait

上面的方法是Object提供的方法,所以任意的Object对象都可以调用,下面来演示一下:

public class ThreadDemo14 {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        System.out.println("wait前");
        obj.wait();
        System.out.println("wait后");
    }
}

结果抛出了一个异常:非法的锁状态异常,也就是调用wait的时候,当前锁的状态是非法的

这是因为,在wait方法中,会先解锁然后再等待,所以要使用wait,就要先加个锁,阻塞等待就是把自己的锁释放掉再等待,不然一直拿着锁等待,其他线程就没机会了


把wait操作写在synchronized方法里就可以了,运行之后main线程就一直等待中,在jconsole中看到的也是waiting的状态

注意:wait操作进行解锁和阻塞等待是同时执行的(打包原子),如果不是同时执行就可能刚解锁就被其他线程抢占了,然后进行了唤醒操作,这时原来的线程再去等待,已经错过了唤醒操作,就会一直等

wait执行的操作:1. 释放锁并进入阻塞等待,准备接收唤醒通知 2. 收到通知后唤醒,并重新尝试获得锁

1.2. notify

接下来再看一下notify方法:

public class ThreadDemo15 {
    private static Object lock = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (lock){
                System.out.println("t1 wait 前");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 后");
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock){
                System.out.println("t2 notify 前");
                Scanner sc = new Scanner(System.in);
                sc.next();//这里的输入主要是构造阻塞
                lock.notify();
                System.out.println("t2 notify 后");
            }
        });
    }
}

然后就会发现又出错了,还是之前的错误,notify也需要先加锁才可以

把之前的notify也加进synchornized就可以了,并且还需要确保是同一把锁

调用wait方法的线程会释放其持有的锁,被唤醒的线程在执行之前,必须重新获取被释放的锁

public class Cook extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    if (Desk.foodFlag == 0) {
                        try {
                            Desk.lock.wait();//厨师等待
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        Desk.count--;
                        System.out.println("还能再吃" + Desk.count + "碗");
                        Desk.lock.notifyAll();//唤醒所有线程
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}
public class Foodie extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    if (Desk.foodFlag == 1) {
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        System.out.println("已经做好了");
                        Desk.foodFlag = 1;
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}
public class Desk {
    public static int foodFlag = 0;

    public static int count = 10;
    //锁对象
    public static Object lock = new Object();
}

这里实现的功能就是,厨师做好食物放在桌子上,美食家开始品尝,如果桌子上没有食物,美食家就等待,有的话,厨师进行等待

sleep() 和 wait() 的区别:

这两个方法看起来都是让线程等待,但是是有本质区别的,使用wait的目的是为了提前唤醒,sleep就是固定时间的阻塞,不涉及唤醒,虽然之前说的Interrupt可以使sleep提前醒来,但是Interrupt是终止线程,并不是唤醒,wait必须和锁一起使用,wait会先释放锁再等待,sleep和锁无关,不加锁sleep可以正常使用,加上锁sleep不会释放锁,抱着锁一起睡,其他线程无法拿到锁

在刚开始提到过,如果有多个线程都在同一个对象上wait,那么唤醒哪一个线程是随机的:

public class ThreadDemo16 {
    private static Object lock = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t1 wait 前");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 后");
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t2 wait 前");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2 wait 后");
            }
        });
        Thread t3 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t3 wait 前");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t wait 后");
            }
        });
        Thread t4 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t4 notify 前");
                Scanner sc = new Scanner(System.in);
                sc.next();
                lock.notify();
                System.out.println("t4 notify 后");
            }
        });
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

这次只是t1被唤醒了

还可以使用notifyAll,把全部的线程都唤醒

2. 阻塞队列

2.1. 阻塞队列的使用

阻塞队列是一种特殊的队列,相比于普通的队列,它支持两个额外的操作:当队列为空时,获取元素的操作会被阻塞,直到队列中有元素可用;当队列已满时,插入元素的操作会被阻塞,直到队列中有空间可以插入新元素。

当阻塞队列满的时候,线程就会进入阻塞状态:

public class ThreadDemo19 {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>(3);
        blockingDeque.put(1);
        System.out.println("添加成功");
        blockingDeque.put(2);
        System.out.println("添加成功");
        blockingDeque.put(3);
        System.out.println("添加成功");
        blockingDeque.put(4);
        System.out.println("添加成功");
    }
}

同时,当阻塞队列中没有元素时,再想要往外出队,线程也会进入阻塞状态

public class ThreadDemo20 {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>(20);
        blockingDeque.put(1);
        System.out.println("添加成功");
        blockingDeque.put(2);
        System.out.println("添加成功");
        blockingDeque.take();
        System.out.println("take成功");
        blockingDeque.take();
        System.out.println("take成功");
        blockingDeque.take();
        System.out.println("take成功");
    }
}

2.2. 实现阻塞队列

根据阻塞队列的特性,可以尝试来自己手动实现一下

可以采用数组来模拟实现:

public class MyBlockingDeque {
    private String[] data = null;
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    public MyBlockingDeque(int capacity) {
        data = new String[capacity];
    }
}

接下来是入队列的操作:

public void put(String s) throws InterruptedException {
    synchronized (this) {
        while (size == data.length) {
            this.wait();
        }
        data[tail] = s;
        tail++;
        if (tail >= data.length) {
            tail = 0;
        }
        size++;
        this.notify();
    }
}

由于设计到变量的修改,所以要加上锁,这里调用wait和notify来模拟阻塞场景,并且需要注意wait要使用while循环,如果说被Interrupted打断了,那么就会出现不可预料的错误

出队列也是相同的道理:

public String take() throws InterruptedException {
String ret = "";
synchronized (this) {
    while (size == 0) {
        this.wait();
    }
    ret = data[head];
    head++;
    if (head >= data.length) {
        head = 0;
    }
    size--;
    this.notify();
}
return ret;
}

3. 生产者消费者模型

生产者消费者模型是一种经典的多线程同步模型,用于解决生产者和消费者之间的协作问题。在这个模型中,生产者负责生产数据并将其放入缓冲区,消费者负责从缓冲区中取出数据并进行处理。生产者和消费者之间通过缓冲区进行通信,彼此之间不需要直接交互。这样可以降低生产者和消费者之间的耦合度,提高系统的可维护性和可扩展性。

而阻塞队列可以当做上面的缓冲区:

public class ThreadDemo21 {
    public static void main(String[] args) {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>(100);
        Thread t1 = new Thread(()->{
            int i = 1;
            while (true){
                try {
                    blockingDeque.put(i);
                    System.out.println("生产元素:" + i);
                    i++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t2 = new Thread(()->{
            while (true){
                try {
                    int i = blockingDeque.take();
                    System.out.println("消费元素:" + i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

如果说把sleep的操作放到线程2会怎么样?

线程一瞬间就把阻塞队列沾满了,后面还是一个线程生产,一个线程消费,虽然打印出来的有偏差

生产者和消费者之间通过缓冲区进行通信,彼此之间不需要直接交互。这样可以降低生产者和消费者之间的耦合度,提高系统的可维护性和可扩展性。 

在这里插入图片描述

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值