Java深入学习29:线程等待和唤醒的两个方案
模拟场景
一个门店,有一个店员,有消费者来消费商品(每次消费1件商品),有仓库人员来添加(生产)商品(每次生产1件商品),并假设库存上限是2.
基础代码实现
public class ThreadNotifyTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Producer producer = new Producer(clerk); Consumer consumer = new Consumer(clerk); new Thread(producer,"生产者A").start(); new Thread(consumer,"消费者B").start(); new Thread(producer,"生产者C").start(); new Thread(consumer,"消费者D").start(); } } class Clerk{ private int proNum = 0; get(){ ......//参考具体方案 } add(){ ......//参考具体方案 } } class Producer implements Runnable{ private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { for(int i=0; i<10; i++){ clerk.add(); } } } class Consumer implements Runnable{ private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { for(int i=0; i<10; i++){ clerk.get(); } } }
方案1:基础版本。使用synchronized ,get 时判断是否缺货,add 时判断是否库满
问题:根据结果发现,在生产、消费过程中会出现大量的库满和缺货情况。
class Clerk{ private int proNum = 0; public synchronized void get(){ if(proNum <= 0){ System.out.println("缺货"); }else{ System.out.println(Thread.currentThread().getName() + ": " + --proNum); } } public synchronized void add(){ if(proNum > 1){ System.out.println("库满"); }else{ System.out.println(Thread.currentThread().getName() + ": " + ++proNum); } } }
方案2:使用 wait 和 notifyAll 方法,解决方案1问题;但是引发了新问题
问题:程序会出现无法终止的情况。原因在于当循环操作在进行到最后时,会出现一个线程一直在等待,没有其他线程唤醒该等待线程。
class Clerk{ private int proNum = 0; public synchronized void get(){ if(proNum <= 0){ System.out.println("缺货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ System.out.println(Thread.currentThread().getName() + ": " + --proNum); try { this.notifyAll(); } catch (Exception e) { e.printStackTrace(); } } } public synchronized void add(){ if(proNum > 1){ System.out.println("库满"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ try { this.notifyAll(); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ": " + ++proNum); } } }
方案3:我们对get和add方法内部进行了优化,去掉了if-else中的else;解决了线程一直等待问题;但同时引入了新的问题;
问题:库存会溢出或者为负数;原因是多个生产者和消费者修改库存,产生了脏数据(比如两个生产者同时被欢迎,执行了++proNum,导致库存溢出);这也是常说的虚假唤醒情况。Object类中的wait()方法也专门针对该情况做了推荐处理(参考方案4)。
class Clerk{ private int proNum = 0; public synchronized void get(){ if(proNum <= 0){ System.out.println("缺货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ": " + --proNum); try { this.notifyAll(); } catch (Exception e) { e.printStackTrace(); } } public synchronized void add(){ if(proNum > 1){ System.out.println("库满"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { this.notifyAll(); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ": " + ++proNum); } }
方案4:使用 while 替换 if 判断,解决虚假唤醒问题
class Clerk{ private int proNum = 0; public synchronized void get(){ while(proNum <= 0){ System.out.println("缺货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ": " + --proNum); try { this.notifyAll(); } catch (Exception e) { e.printStackTrace(); } } public synchronized void add(){ while(proNum > 1){ System.out.println("库满"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { this.notifyAll(); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ": " + ++proNum); } }
方案5:使用 Lock 锁和 Condition
class Clerk{ private int proNum = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void get(){ lock.lock(); try { while(proNum <= 0){ System.out.println("缺货"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ": " + --proNum); try { condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } }finally { lock.unlock(); } } public void add(){ lock.lock(); try { while(proNum > 1){ System.out.println("库满"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } try { condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ": " + ++proNum); }finally { lock.unlock(); } } }
总结
1- 方案4和方案5,针对模拟场景,是比较合理的方案。
几个概念:
1- Java中对象锁的模型
java中对象锁的模型,JVM会为一个使用内部锁(synchronized)的对象维护两个集合,Entry Set和Wait Set,也有人翻译为锁池和等待池,意思基本一致。对于Entry Set:如果线程A已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,它只能进入Entry Set,并且处于线程的BLOCKED状态。对于Wait Set:如果线程A调用了wait()方法,那么线程A会释放该对象的锁,进入到Wait Set,并且处于线程的WAITING状态。还有需要注意的是,某个线程B想要获得对象锁,一般情况下有两个先决条件,一是对象锁已经被释放了(如曾经持有锁的前任线程A执行完了synchronized代码块或者调用了wait()方法等等),二是线程B已处于RUNNABLE状态。那么这两类集合中的线程都是在什么条件下可以转变为RUNNABLE呢?对于Entry Set中的线程,当对象锁被释放的时候,JVM会唤醒处于Entry Set中的某一个线程,这个线程的状态就从BLOCKED转变为RUNNABLE。对于Wait Set中的线程,当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某一个线程,这个线程的状态就从WAITING转变为RUNNABLE;或者当notifyAll()方法被调用时,Wait Set中的全部线程会转变为RUNNABLE状态。所有Wait Set中被唤醒的线程会被转移到Entry Set中。然后,每当对象的锁被释放后,那些所有处于RUNNABLE状态的线程会共同去竞争获取对象的锁,最终会有一个线程(具体哪一个取决于JVM实现,队列里的第一个?随机的一个?)真正获取到对象的锁,而其他竞争失败的线程继续在Entry Set中等待下一次机会。
2- wait()方法外面为什么是while循环而不是if判断
我们在调用wait()方法的时候,心里想的肯定是因为当前方法不满足我们指定的条件,因此执行这个方法的线程需要等待直到其他线程改变了这个条件并且做出了通知。那么为什么要把wait()方法放在循环而不是if判断里呢,其实答案显而易见,因为wait()的线程永远不能确定其他线程会在什么状态下notify(),所以必须在被唤醒、抢占到锁并且从wait()方法退出的时候再次进行指定条件的判断,以决定是满足条件往下执行呢还是不满足条件再次wait()呢。
3- notifyAll() 和 notify() 区别
notify()是唤醒一个线程,notifyAll()是唤醒全部线程。notify()非常容易导致死锁,
附录1-方案日志
END