-
在刚接触Java线程的时候,对于wait(),notify()以及notifyAll()方法,synchronized修饰符并不是很了解,后面学习到一个例子也称作为生产者消费者模型,下面通过这样一个模型来进行进一步理解线程安全相关的知识。
-
模拟这样一个场景:有一个中心仓库,生产者将生成的商品放入仓库中,而消费者则可以在仓库中获取商品。消费者可以有多个。
实现过程:
-
模拟一个仓库:(为了简易一些就将一个字符串"商品"代替一个商品吧)
public class WareHouse { //存储商品 private ArrayList<String> list = new ArrayList<>(); //生产者添加商品 public void add(){ if(list.size()<20){//当少于20件则继续生产 list.add("商品"); }else { return; } } //消费者获取商品 public void get(){ if(list.size()>0){ list.remove(0); }else { return; } } }
-
模拟生产者:
public class Producer extends Thread{ private WareHouse wareHouse;//构造对象的时候将仓库赋值,保证消费者和生产者使用的是同一个仓库 public Producer(WareHouse wareHouse){ this.wareHouse = wareHouse; } //生产者不断的向仓库放入商品 public void run(){ while (true){ wareHouse.add(); System.out.println("===================>生产者生产了一个商品"); try { Thread.sleep(200);//每隔0.2秒生产一件 } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
模拟消费者:
public class Consumer extends Thread{ private WareHouse wareHouse;//构造对象的时候将仓库赋值,保证消费者和生产者使用的是同一个仓库 public Consumer(WareHouse wareHouse){ this.wareHouse = wareHouse; } public void run(){ while (true){ wareHouse.get(); System.out.println("消费者拿了一件商品"); try { Thread.sleep(300);//每隔0.3秒拿一件 } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
测试:
public class testMain { public static void main(String[] args) { WareHouse wareHouse = new WareHouse(); Producer producer = new Producer(wareHouse); Consumer consumer1 = new Consumer(wareHouse); Consumer consumer2 = new Consumer(wareHouse); producer.start(); consumer1.start(); consumer2.start(); } }
-
这样设计,由于ArrayList本身也是线程非安全的,可以并发访问,在加上有多个消费者,多线程同时访问,极有可能出现消费者判断的时候有商品,而拿的时候由于判断过程到拿的过程中这一瞬间被其他消费者拿了,导致拿的时候拿空的问题。
让我们测试一下:
-
果然出了问题,可以看到访问元素越界,这就是多线程并发,线程非安全问题导致的结果。
-
这个时候我们会想到使用线程安全锁相关的知识synchronized关键字。我们在添加和获取商品的方法上都加上锁。它的作用是用来锁定当前对象的,当有某个对象调用此此对象方法时,此对象被上锁,其他对象不能够访问。这样便可以让线程同步效果,单个时刻只能单个对象调用此方法。
此外,并且在在商品满了或者没有商品的时候让线程挂起。这时我们就是想到wait()方法。也给他添加上去。public class WareHouse { //存储商品 private ArrayList<String> list = new ArrayList<>(); //生产者添加商品 public synchronized void add(){ if(list.size()<20){//当少于20件则继续生产 list.add("商品"); }else { try { this.wait();//让生产者线程挂起 } catch (InterruptedException e) { e.printStackTrace(); } } } //消费者获取商品 public synchronized void get(){ if(list.size()>0){ list.remove(0); }else { try { this.wait();//让当前消费者线程挂起 } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
再次测试: 偶然发现一个问题!!!,如下。当生产者生产够了20件商品的时候,生产者线程碎觉了(挂起),然后消费者接着一直拿,直到没有了商品,但是发现此时消费者没有被唤醒,导致一直处于挂起状态,所以并没有生产商品了。而消费者没有商品拿了之后,也睡着了(挂起)。此时所有线程并没有结束,而是处于假死状态。
-
这个时候我们就会想到notify()方法和notifyAll()方法,用于唤醒线程的方法,由于这里消费者和生产者有可能多个这里用到notifyAll();在消费者没有商品拿的时候就唤醒生产者线程,然后自己碎觉;在生产者生产商品满了的时候就唤醒消费者线程告诉他们可以拿商品了,然后自己碎觉。这样便可处于一个高效并且安全线程并发和交替过程了。
public class WareHouse { //存储商品 private ArrayList<String> list = new ArrayList<>(); //生产者添加商品 public synchronized void add(){ if(list.size()<20){//当少于20件则继续生产 list.add("商品"); }else { try { this.notifyAll();//挂起之前唤醒其他线程干活 this.wait();//让生产者线程挂起 } catch (InterruptedException e) { e.printStackTrace(); } } } //消费者获取商品 public synchronized void get(){ if(list.size()>0){ list.remove(0); }else { try { this.notifyAll();//挂起之前唤醒其他线程干活 this.wait();//让当前消费者线程挂起 } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
总结:
1,上面此模型,通过加锁(synchronized)解决这个模型的解决线程安全问题。还通过(wait(),notifyAll())解决了线程挂起,唤醒线程的过程,从而实现线程切换过程。
2,对于此模型中,当然也可以直接使用线程安全的容器Vector代替ArrayList,利用容器本身特点从而实现并发线程安全问题。
3,在有些时候线程并发非安全是不予考虑,但是在有些时候我们需要避免线程安全问题,要适应不同的场景进行处理。