多线程的开发中有一个最经典的操作案例, 就是生产者-消费者,生产者不断生产产品, 消费者不断取走产品。
一,多线程的关键点回顾
1.1 什么是进程?
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次 执行过程,它是一个动态的概念。
进程是一个具有一定独立功能的程序,一个实体,每一个进程都有它自己的地址空间。
1.2 进程的状态
运行中的进程具有以下三种基本状态:
- 就绪状态(Ready)
- 运行状态(Running)
- 阻塞状态(Blocked)
1.3 线程实现的方式
实现方式1:继承Thread类
class MyThread extends Thread{
public void run(){
//逻辑处理
}
}
MyThread mt = new MyThread();
mt.start();
实现方式2:实现Runnable接口
class MyRunnable implements Runnable{
public void run(){
//逻辑处理
}
}
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();
1.4 线程的状态
- New(初始化状态)
- Runnable(可运行/运行状态)
- Blocked(阻塞状态)
- Dead(终止状态)
注意一个区别:
线程的休眠:使用sleep方法实现,使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),释放CPU的时间片,具体取决于系统定时器和调度程序的精度和准确性。 线程不会丢失任何显示器的所有权。
线程等待:使用wait方法实现,让线程暂时停止执行,释放CPU的时间片,并释放对象监视器的所有权,等待其它线程通过 notify方法来唤醒。
二,生产者与消费者案列分析
饭店里的有一个厨师和一个服务员,这个服务员必须等待厨师准备好膳食。 当厨师准备好时,他会通知服务员,之后服务员上菜,然后返回继续等待。
这是一个任务协作的示例,厨师代表生产者,而服务员代表消费者。
//食物(两个线程需要共享的数据对象)
class Food{
private String name; //菜名
private String description; //菜的功效
private float price; //单价
private boolean flag = true; //true表示可以生产,false表示可以消费
public Food() {
}
public Food(String name, String description, float price) {
this.name = name;
this.description = description;
this.price = price;
}
/**
* 生产食物
* @param name
* @param description
* @param price
* 同步:对象锁,以当前对象上标记一个锁
*/
public synchronized void set(String name,String description, float price){
//当前线程不能生产食物
if(!flag){
try {
this.wait(); //线程进入等待状态,释放对象监视器的所有权(对象锁)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.setName(name);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setDescription(description);
this.setPrice(price);
flag = false;//表示可以消费了
this.notify();//唤醒正在等待的另一个线程
}
/**
* 消费食物
*/
public synchronized void get(){
//不能消费
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName()+"-"+this.getDescription()+"-"+this.getPrice());
flag = true; //可以生产了
this.notify();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
@Override
public String toString() {
return "Food{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
", price=" + price +
'}';
}
}
//生产者对象
class Producer implements Runnable{
private Food food;
public Producer(Food food) {
this.food = food;
}
@Override
public void run() {
for (int i=0;i<20;i++){
if (i%2==0){
food.set("韭菜焖鸡蛋","程序员的最爱,补啊",50f);
}else{
food.set("凉拌肾宝","男人的最爱,大补啊",200f);
}
}
}
}
//消费者对象
class Consumer implements Runnable{
private Food food;
public Consumer(Food food) {
this.food = food;
}
@Override
public void run() {
for (int i=0;i<20;i++){
food.get();
}
}
}
public class ProducterConsumerDemo {
public static void main(String[] args) {
Food food = new Food();
Producer p = new Producer(food);
Consumer c = new Consumer(food);
Thread tp = new Thread(p);
Thread cp = new Thread(c);
tp.start(); //不是启动线程,表示当前线程进入就绪状态,随时等待 CPU 调度
cp.start();
}
}
测试结果如下:
三,多线程的经典面试题
1、生产者消费者模型的作用是什么
- 通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用
- 解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费 者之间的联系少,联系越少越可以独自发展而不需要受到相互的制约
2、sleep方法和wait方法有什么区别
- 使用sleep方法实现,使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),释放CPU的时间片,具体取决于系统定时器和调度程序的精度和准确性。 线程不会丢失任何显示器的所有权。
- 使用wait方法实现,让线程暂时停止执行,释放CPU的时间片,并释放对象监视器的所有权,等待其它线程通过 notify方法来唤醒。
3、如何在两个线程之间共享数据
-
方式一:将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加synchronized
-
方式二:将Runnable对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个 Runnable 对象调用外部类的这些方法。
4、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
- 要明白,每个对象都可以被认为是一个"监视器monitor",这个监视器由三部分组成(一个独占锁,一个入口队列,一个等待队列)。注意是一个对象只能有一个独占锁,但是任意线程线程都可以拥有这个独占锁。
- 对于对象的非同步方法而言,任意时刻可以有任意个线程调用该方法。(即普通方法同一时刻可以有多个线程调用)
- 对于对象的同步方法而言,只有拥有这个对象的独占锁才能调用这个同步方法。如果这个独占锁被其他线程占用,那么另外一个调用该同步方法的线程就会处于阻塞状态。
- 若一个拥有该独占锁的线程调用该对象同步方法的wait()方法,则该线程会释放独占锁,并加入对象的等待队列;
- 某个线程调用notify(),notifyAll()方法是将等待队列的线程转移到入口队列,然后让他们竞争锁。
5、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别
- wait():导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法或者指定的事件用完
- notify():唤醒在此对象监视器上等待的单个线程
- notifyAll():唤醒在此对象监视器上等待的所有线程
- wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
- wait() 与 notify/notifyAll 方法必须在同步代码块中使用。
- 由于 wait() 与 notify/notifyAll() 是放在同步代码块中的,因此线程在执行它们时,肯定是进入了临界区中的,即该线程肯定是获得了锁的。
- 当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。
- 当执行notify/notifyAll方法时,会唤醒一个处于等待该 对象锁 的线程,然后继续往下 执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁。
- 如果在执行wait() 与 notify/notifyAll() 之前没有获得相应的对象锁,就会抛出:java.lang.IllegalMonitorStateException异常。
6、Thread.sleep(0)的作用是什么
- 相当于yield(),使当前线程从运行状态转入就绪状态,CPU会重新选择线程执行,并且刚刚的线程有可能被选中。
来源:CSDN
作者:木讷的鱼
链接:https://blog.csdn.net/weixin_44726976/article/details/103958261