提示:本文带大家入门JavaSE基础的多线程部分,更深层次建议看书籍《Java并发编程》
全文主要内容为:
1.wait() notify() 的使用
2.生产消费者模型:面试常问
3.常见的问题
本人能力有限,如有遗漏或错误,敬请指正,谢谢
文章目录
其他文章
1.Java多线程基本概念和常用API(面试高频)
2.线程安全问题(synchronized解决,各种类型全)
3.Java并发线程池使用和原理(通俗易懂版)
4.基于SpringBoot+Async注解整合多线程
前言
学习一门技术最好使用wwh方法
what:这门技术是什么
why:为什么用这个技术,使用会有什么优化
how:怎么使用
提示:以下是本篇文章正文内容,下面有代码案例可供参考,直接复制就能运行
一、前置知识
1.1 wait()方法
wait()是Object类的方法,调用wait()会导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量自动停止等待
大白话:wait()就是暂停当前正在执行的线程,并释放同步锁synchronized,让其他线程可以有机会运行
关键点:程序停顿在这一行,线程变为上图中的阻塞状态
具体怎么用看下面案例
1.2 notify()方法
notify()是Object类的方法,唤醒被wait()暂停的线程,比如线程A调用wait()暂停后,过了一段时间后想让他重新启动,就调用notify()。
不足:如果有多个线程被wait, 就唤醒优先级高的,也就是说不能指定唤醒某一个线程
大白话:员工请假(wait),老板叫他过来上班(notify)
关键点:线程状态从阻塞状态变为就绪状态
具体怎么用看下面案例
1.3 notifyAll()方法
和上面一样,只不过一旦执行此方法,就会唤醒所有被wait()的线程。
二、线程通信
正常情况下,每个子线程完成各自的任务就可以结束了。不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信
下面是举例说明:
2.1 两个线程轮流打印
需求:线程A打印A,线程B打印B,现在要求两个线程轮流打印十次
输出:ABABABABAB…
思路分析:对于“打印println()”这个动作,相当于共享变量,不可能两个线程随意执行,所以要加锁,轮流占有锁,这样就能轮流打印了
public static void main(String[] args) {
//锁对象
Object o = new Object();
Thread A = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
synchronized (o) {
System.out.println("A");
}
}
});
Thread B = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
synchronized (o) {
System.out.println("B");
}
}
});
A.start();
B.start();
}
那么现在加锁后,能保证同一时间只有一个线程在打印(谁持有锁就谁打印)
现在问题在于如何释放锁,并且释放锁后,保证锁由另外一个线程获取到(因为两个线程都可以抢占锁,即使是刚刚释放锁的线程)
思路分析:使用上面的wait()和notify(),使用wait()可以释放锁。
- 假设现在A拿到锁,打印完“A”后,调用wait()方法,自己会处于阻塞状态,无法抢占锁。另一个线程B这时候就一定能拿到锁(因为A线程处于阻塞)
- 等拿到锁打印完“B”后,就执行notify()唤醒线程A,由于唤醒线程A后,锁还是B拿着,A进不去,这时候B就要调用wait()方法,释放锁同时自己阻塞
所以只需要记住:业务-唤醒-释放锁等待
public static void main(String[] args) {
//锁对象
Object o = new Object();
Thread A = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
synchronized (o) {
//1.业务
System.out.println("A");
//2.唤醒(调用者为什么是o这个对象,看下文)
o.notify();
//3.释放锁等待:wait()方法是需要捕获异常的
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread B = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
synchronized (o) {
//1.业务
System.out.println("B");
//2.唤醒
o.notify();
//3.释放锁等待:wait()方法是需要捕获异常的
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
A.start();
B.start();
}
2.2 常见面试题
1.wait()和notify()的调用者必须是锁对象,并且只能在synchronized内使用
答:调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。如果wait()方法在外部使用的话,会报异常
2.wait方法释放锁,被唤醒后,醒来的位置就是wait所在的位置
3.sleep()和wait()区别
1.sleep()是Thread类的方法,wait()是Object类的方法
2.sleep是使线程休眠,不会释放对象锁;wait是使线程等待,释放锁,sleep让出的是cpu,如果此时代码是加锁的,那么即使让出了CPU,其他线程也无法运行,因为没有得到锁;wait是让自己暂时等待,放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
3.sleep()可以用在任何位置,wait()只能用在同步代码块或者同步方法中
三、生产消费者模型(经典面试手写※)
需求:现在有一个生产者生产商品(商品增加一个),当商品数量大于0的时候,消费者就会消费(商品减少一个),循环十次
思路:业务-唤醒-释放锁等待
在实际多线程开发的时候,资源类和线程类是分开的,不要直接把业务写在run()方法中
编写商品类:商品里有增加商品和减少商品方法
public class Product {
//商品数量
public int count;
public Product(int count){
this.count=count;
}
//增加商品:同步方法
public synchronized void add(){
//商品数量>0说明生产者不需要干任何事,所以如果抢到锁就直接等待
if(this.count>0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//等生产者被唤醒后代码就会到这里,说明要生产了
System.out.println("生产者开始生产商品,当前商品数量为:"+this.count);
this.count++;
System.out.println("生产者生产完商品,当前商品数量为:"+this.count);
System.out.println("------------");
//生产完通知消费者消费
this.notify();
}
//消费商品:同步方法
public synchronized void pop(){
//当商品<=0的时候,所以没有商品,消费者无法消费,所以如果抢到锁就等待
if(this.count<=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒后,有商品了所以开始消费
System.out.println("消费者开始消费商品,当前商品数量为:"+this.count);
this.count--;
System.out.println("消费者消费商品后,当前商品数量为:"+this.count);
System.out.println("------------");
this.notify();
public static void main(String[] args) {
//假设现在有5个商品,那么也就是说线程启动,一定是消费者先消费
Product product=new Product(5);
//生产者线程
Thread producer=new Thread(() -> {
for(int i=0;i<10;i++){
//线程类和资源类分开,只需要调用业务的方法
product.add();
}
},"生产者");
//消费者线程
Thread consumer=new Thread(() -> {
for(int i=0;i<10;i++){
product.pop();
}
},"消费者");
producer.start();
consumer.start();
}
可以看到当初始化的商品有5个的时候,那么生产者是不会做任何操作的(因为执行了wait()方法)
当消费者消费商品到0的时候,生产者开始生产,之后由于生产者每生产一个,消费者感知到就消费了,所以最后面是交替执行
四、多个生产消费者模型可能出现的问题:虚假唤醒
现在假设有两个生产者,两个消费者 ,场景和上面一样
//新增生产者B,消费者B
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
product.push();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
product.pop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者B").start();
实际运行结果:没有同步的效果
生产者A添加产品,剩余1件产品
消费者A使用产品,剩余0件产品
生产者B添加产品,剩余1件产品
生产者A添加产品,剩余2件产品
生产者B添加产品,剩余3件产品
消费者A使用产品,剩余2件产品
消费者A使用产品,剩余1件产品
...
4.1 虚假唤醒
问题在于线程被唤醒后的位置=wait()方法调用的位置,就是从哪里等待就从哪里醒来。
if(this.count>0){
try {
this.wait();//线程被唤醒后,从这里继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
那么现在场景如下:
1.生产者A抢到锁生产产品(产品数量:1),释放锁后,生产者A又抢到锁,由于产品数量为1,进入if语句进行等待。
2.然后消费者A抢到锁进行消费(产品数量:0)
3.生产者B抢到锁进行生产(产品数量:1),此时释放锁。
4.生产者A抢到锁,这时候关键来了:生产者醒来的位置在if语句的下面,重新抢到时间片进行方法调用也是从这里开始,然后由于不用进行if判断,又可以生产了(产品数量:2)
//现在是生产者A抢到锁进入方法:此时产品数量为1
if(this.count>0){
try {
this.wait();//生产者A在这里醒来,抢到时间片后继续从这里开始执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//添加产品代码:生产者A又生产了一个产品
解决办法:if改成while,这样就算醒来也还是要再判断一次
while(this.count>0){
try {
this.wait();//生产者A在这里醒来,抢到时间片后,由于是while循环会继续判断
} catch (InterruptedException e) {
e.printStackTrace();
}
}
总结:两个线程(生产者+消费者)不会出现虚假唤醒问题,四个或多个线程才会出现