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 {@Overridepublic 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 {@Overridepublic 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会怎么样?
线程一瞬间就把阻塞队列沾满了,后面还是一个线程生产,一个线程消费,虽然打印出来的有偏差
生产者和消费者之间通过缓冲区进行通信,彼此之间不需要直接交互。这样可以降低生产者和消费者之间的耦合度,提高系统的可维护性和可扩展性。