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