文章目录
wait、notify、堵塞队列、定时器
1 wait、notify
wait 和 notify都属于Object类的方法(任何对象都可以调用)
Object类的方法:
clone(); equals(Object obj);
finalize();getClass();
hashCode();notify();notifyAll();
toString();wait();
作用:
Object o=new Object();
当A线程调用o.wait()
之后,A线程会放弃CPU,从就绪队列移到o指向对象的等待集(wait sets)中,并且失去抢夺CPU的资格(状态从Runnable切换到Waiting)
当B线程调用o.notify()
之后,会把o指向对象的等待集(wait sets)上的任意一个线程,从等待集移到就绪队列中,并且把这个线程的状态从Waiting切换到Runnable,使这个线程重新拥有了抢CPU的资格。
当B线程调用 o.notifyAll()
时,会把等待集上的所有线程唤醒。
wait和notify演示:
import java.util.Scanner;
public class UsageOfWait {
static Object o = new Object();
static class Print extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 50; i++) {
System.out.println(i);
if (i == 30) {
// 我等在 o 上
synchronized (o) { // 把 o 锁起来
o.wait();
} // 才会释放锁
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Print print = new Print();
print.start();
Thread.sleep(2 * 1000);//等待wait调用结束,再执行notify
System.out.println("输入任意字符,以唤醒 Print 线程");
Scanner scanner = new Scanner(System.in);
scanner.nextLine();
synchronized (o) {
o.notify();
}
}
}
o调用wait时:
1 更改线程的状态
2 把线程移到o的等待集上
3 把o中的锁释放
4 当前线程放弃CPU
…
5 当前线程重新用于CPU,继续执行。
6 再次请求o中的锁
7 当请求锁成功时,wait调用结束。
o调用notify时:
会首先唤醒,等待集中的一个线程,等待notify中的代码块执行结束后,才会释放锁。
注意:
- 要确保notify在wait之后执行,notify只能唤醒当前时间点上正在等待的线程。
- 使用wait和notify的线程都需要使用synchronized锁
- wait被调用时会unlock这个锁,当被notify唤醒时,需要重新lock这个锁。
2 堵塞队列(Blocking Queue)
(1) 常用的原生堵塞队列:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.PriorityBlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> q1 = new ArrayBlockingQueue<>(100);
BlockingQueue<String> q2 = new LinkedBlockingDeque<>();
BlockingQueue<String> q3 = new PriorityBlockingQueue<>();
q1.put("");
String take = q1.take();
System.out.println(take);
}
}
ArrayBlockingQueue
循环阻塞队列
LinkedBlockingQueue
链表实现,没有最大容量限制(最大长度是int的最大值)
PrioriyBlockingQueue
堆实现,优先级队列堵塞队列
注意:
Java原生的阻塞队列——CTRL + H查看类的继承层次。
自定义阻塞队列——以循环队列(尾插头出)举例:
用wait-notify机制来完成堵塞队列。
(2)单生产者单消费者模型(单线程):
线程不安全版本:
public class MyBlockingArrayQueue {
int[] array = new int[10];
int front = 0;
int rear = 0;
int size = 0;
void put(int value) {
// 考虑满的情况
if (size == array.length) {
// 队列已满
throw new RuntimeException("队列已满");
}
array[rear] = value;
rear = (rear + 1) % array.length;
size++;
}
int take() {
// 考虑空的情况
if (size == 0) {
throw new RuntimeException("队列已空");
}
int value = array[front];
front = (front + 1) % array.length;
size--;
return value;
}
}
(3) 加入锁和wait和notify机制的线程安全版本:
生产者(put)在队列满时等待, 消费者(take)在队列空时等待.
import java.util.Scanner;
public class MyBlockingArrayQueue {
int[] array = new int[10];
//下标处的数据可能出现生产者和消费者修改同一处的情况
int front = 0; // 只有消费者修改 front
int rear = 0; // 只有生产者修改 rear
int size = 0; // size 是生产者消费者都会修改的
// 生产者才会调用 put
synchronized void put(int value) throws InterruptedException {
// 考虑满的情况
if (size == array.length) {
// 队列已满
//throw new RuntimeException("队列已满");
wait();
}
array[rear] = value;
rear++;
if (rear == array.length) {
rear = 0;
}
//rear = (rear + 1) % array.length;
size++; // 我们需要保障的是 size++ 的原子性,所以 volatile 无法解决
notify();
}
// 调用 take 的一定是消费者
synchronized int take() throws InterruptedException {
// 考虑空的情况
if (size == 0) {
// 空的
//throw new RuntimeException("队列已空");
wait();
}
int value = array[front];
front++;
if (front == array.length) {
front = 0;
}
//front = (front + 1) % array.length;
size--;
notify();
return value;
}
static MyBlockingArrayQueue queue = new MyBlockingArrayQueue();
static class Producer extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
System.out.println("准备放入 " + i);
queue.put(i);
System.out.println("放入成功");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Producer producer = new Producer();
producer.start();
Thread.sleep(2 * 1000);
Scanner scanner = new Scanner(System.in);
while (true) {
scanner.nextLine();
System.out.println(queue.take());
}
}
}
当调用put队列满时用wait()等待,当take发生后,调用notify,唤醒put线程,达到了一个取一个放一个的效果。.
注意:
- 普通方法中访问普通属性、调用普通方法的正式用法是
this.属性; this.方法();
当属性或方法命名没有冲突时,this可以省略直接采用属性;方法();
的形式 - notify在size++之前唤醒或之后唤醒没有区别。
(4)多生产者多消费者(多线程):
线程不安全版本:
import java.util.Scanner;
public class MyBlockingArrayQueueWrongDemo {
// 定义个队列对象-生产者线程是 Producer,消费者线是 main 线程
// 队列是需要在生产者消费者之间共享的
static MyBlockingArrayQueue queue = new MyBlockingArrayQueue();
// 定义一个生产者线程类
static class Producer extends Thread {
@Override
public void run() {
try {
int i = 0;
while (true) {
queue.put(i);
i++;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Producer producer1 = new Producer();
producer1.start();
Producer producer2 = new Producer();
producer2.start();
Producer producer3 = new Producer();
producer3.start();
while (true) {
queue.take();
}
}
}
比如现在有三个生产者(put),一个消费者(take)
当三个put都执行完毕后,都卡在wait这里
if (size == array.length) {
// 队列已满
//throw new RuntimeException("队列已满");
wait();
}
当take线程取走一个时,唤醒了其中一个put线程,此时这个put线程写到满的时候,恰巧又唤醒了另一个写线程。因为写线程都卡在wait这里,当wait重新获得锁时,wait执行结束,会继续向下执行,而忽略了判断size == array.length。从而导致了size一直增大。
为了处理这种情况,我们将if换成while让wait结束后继续进行判断。
while (size == array.length) {
// 队列已满
//throw new RuntimeException("队列已满");
wait();
}
但是更改了while后还存在问题,可能会出现3个put和1个take都在等待集上的情况。还是因为生产者调完后,唤醒的不一定是消费者造成的。
即当take将put的值全部取完后,也进入了等待集中,而唤醒的put写满之后又唤醒了一个put,此时因为有while判断,所以put还是会继续在等待集上,此时四个线程全部都在等待集上,那么就没有线程去执行notify了.
要将notify修改为notifyall,即当每次put或take时都将等待集中的线程唤醒。
(5) 修改之后的多生产者消费者模型:
import java.util.Scanner;
public class MyBlockingArrayQueue {
int[] array = new int[1]; // 下标处的数据可能出现生产者和消费者修改同一处的情况
int front = 0; // 只有消费者修改 front
int rear = 0; // 只有生产者修改 rear
int size = 0; // size 是生产者消费者都会修改的
// 生产者才会调用 put
synchronized void put(int value) throws InterruptedException {
// 考虑满的情况
while (size == array.length) {
// 队列已满
//throw new RuntimeException("队列已满");
wait();
}
// 通过 while 循环,保证了,走到这里时,size 一定不等于 array.length
array[rear] = value;
rear++;
if (rear == array.length) {
rear = 0;
}
//rear = (rear + 1) % array.length;
size++; // 我们需要保障的是 size++ 的原子性,所以 volatile 无法解决
System.out.println(size); // 1 - 10
notifyAll(); // 我们以为我们唤醒的是消费者线程,但实际可能唤醒了生产者线程
}
// 调用 take 的一定是消费者
synchronized int take() throws InterruptedException {
// 考虑空的情况
while (size == 0) {
// 空的
//throw new RuntimeException("队列已空");
wait();
}
int value = array[front];
front++;
if (front == array.length) {
front = 0;
}
//front = (front + 1) % array.length;
size--;
System.out.println(size); // 0 - 9
notifyAll();
return value;
}
}
注意:
- wait一般要配合着while使用
- 如果要保证精确唤醒想要的线程,需要通过notifyAll来全部唤醒
(6) 手写生产者和消费者
public class B {
public static void main(String[] args) throws Exception {
Data data = new Data();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Data{ // 资源类
private int number = 0;
public synchronized void increment() throws InterruptedException {
// 判断该不该这个线程做
while (number!=0){
this.wait();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
// 通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 判断该不该这个线程做
while (number==0){
this.wait();
}
// 干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
// 通知
this.notifyAll();
}
}
3 定时器
(1)Java中的定时器
java.util.TimerTask
;和java.util.Timer
;
Timer timer = new Timer();
创建一个新的定时器
TimerTask() :
创建一个新的计时器任务。
schedule(TimerTask task, long delay)
:在指定的延迟之后安排指定的任务执行。
10s之后执行定时任务:
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
public class JavaTimerDemo {
static class MyTask extends TimerTask {
@Override
public void run() {
System.out.println("已经是 10 秒之后了");
}
}
public static void main(String[] args) throws InterruptedException {
TimerTask task = new MyTask();
Timer timer = new Timer();
timer.schedule(task, 10 * 1000);
int i = 0;
while (true) {
System.out.println(i++);
TimeUnit.SECONDS.sleep(1);
}
}
}
(2)自定义的定时器:
import java.util.concurrent.TimeUnit;
public class SimpleTimer {
//定时任务接口
interface SimpleTimerTask {
void run();
}
//定时器执行任务
static class Worker extends Thread {
long delay;
SimpleTimerTask task;
Worker(SimpleTimerTask task, long delay) {
this.task = task;
this.delay = delay;
}
@Override
public void run() {
try {
Thread.sleep(delay);
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//构造schedule方法,开启任务
void schedule(SimpleTimerTask task, long delay) {
Worker worker = new Worker(task, delay);
worker.start();
}
//实现SimpleTimerTask接口,构造一个任务
static class MyTask implements SimpleTimerTask {
@Override
public void run() {
System.out.println("10 秒之后");
}
}
public static void main(String[] args) throws InterruptedException {
SimpleTimer timer = new SimpleTimer();
MyTask task = new MyTask();
timer.schedule(task, 10 * 1000);
int i = 0;
while (true) {
System.out.println(i++);
TimeUnit.SECONDS.sleep(1);
}
}
}
自定义定时器的缺点: 每提交一个任务就会创建一个线程
Java中的Timer:Java中的Timer只有一个工作线程,利用优先级堵塞队列存储队列存储收到的任务,每次取到的线程都是定时快要结束的线程(利用了最小堆的特性)。