前言:
多线程并发操作时,有可能会产生两个线程同时操作同一个数据的情况,例如仓库只剩下一件货物,两个消费者同时取这一件货物,这个时候就会产生问题,这也就是所谓的线程不安全
类的设计
- 仓库类(包含一个集合 因为我们要观察运行中产生的问题,所以使用ArrayList集合)
- 生产者类(包含向集合中添加元素的方法)
- 消费者类(包含向集合中取出元素的方法)
以下为代码实现
仓库类
public class Warehouse {
//私有的集合(仓库)
private List<String> list = new ArrayList<String>();
//向集合(仓库)添加元素的方法
public synchronized void add(){
// 如果集合中的数据个数还没有到20那么就向集合中添加一个元素
if(list.size()<20){
list.add("a");
}else{
try {
//this.notifyAll();
// 如果集合中元素数量大于20,那么就让线程进入等待状态
// 这个地方需要注意,this在这里并不是指仓库对象,而是操作仓库对象的那个线程
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//从集合中去元素的方法
public synchronized void del(){
if(list.size()>0){
list.remove(0);
}else{
try {
//this.notifyAll();
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在仓库类中,无论是添加元素还是取出元素,在满足了条件之后,我们需要让线程转换成等待状态,有关其中this.wait() this并不是指仓库对象,而是指一个线程,这点需要注意
生产者类与消费者类只需要重写run()方法(由于生产者和消费者不会只有一个,所以这里我们将两者都继承Thread)
这里我只放了消费者的代码,因为两者代码基本相同,无非一个存,一个取
public class Consumer extends Thread {
private Warehouse w;
public Consumer(Warehouse w){
this.w = w;
}
public void run(){
while(true){
w.del();
System.out.println("消费者取出了一件商品");
try {
Thread.sleep(300); // 消费者取出商品后休息一下
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里要注意一点,我并没有使用单例模式,而是将仓库对象作为了一个属性,放在了生产者与消费者类中,在最后测试时,创建生产者消费者时,只需要将同一个仓库对象传进去,就可以保证操作的是同一个仓库,当然,如果想使用单例模式也是可以的
测试运行
- 以上的代码,看似没什么问题,但实际运行的时候会出现生产者把仓库装满之后,进入了等待状态,而消费者一直取物品,取完之后,就没东西可取了,也进入了等待状态,也就相当于是一种假死状态
这时候我们可以看到,程序还在运行中,但是两者都进入了等待状态
所以我们必须在生产者或者消费者等待之前,先把对方叫醒 this.notifyAll();
public synchronized void del(){
if(list.size()>0){
list.remove(0);
}else{
try {
//在这里先唤醒其他人,自己再进入等待
this.notifyAll();
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果
可以看到,消费者休息后,生产者会起床开始工作,放满仓库再回去休息,消费者再来取出物品,一直循环下去
不知道大家有没有注意到仓库中的方法中的synchronized关键字,这个关键字就是用来保证线程安全的,我们来看一下没有这个关键字执行的结果是什么
我们可以看到程序报出了IllegalMonitorStateException的异常,这个异常的意思,就是在调用wait()方法的时候,本来是要告诉生产者线程:你要休息了,可线程突然变成了消费者的线程,这个时候就会出现该异常,所以我们必须要给方法上加上锁(synchronized),这个时候,我们的程序的基本功能才算实现