Java 生产者与消费者多线程实现——以及问题解析

问题描述:

       模拟实现一种场景:有一个篮子,最多可以装5个苹果,一个人从篮子中取苹果,一个人向篮子中放苹果。

问题思路:

      线程同步的经典问题——生产者和消费者,只不过换了个皮囊。此处只有一个生产者和一个消费者。


初步实现,不罗嗦,直接上代码:

public class TestInteger {

	private Integer number = 0;//篮子中苹果的数量
	class Consumer implements Runnable {//消费者
		@Override
		public void run() {
			// TODO Auto-generated method stub
			while (true) {
				while (number > 0) {
					number--;
					System.out.println("---------------------------------------------"+ number);
				}
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}

		}

	}

	class Producer implements Runnable {//生产者

		@Override
		public void run() {
			// TODO Auto-generated method stub
			while (true) {
				while (number < 5) {
					number++;
				<span style="white-space:pre">	</span>System.out.println("++++++++++++++++++++++++++++++++++++++++++++++"+ number);
					
				}
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}

	public static void main(String[] args) {
		TestInteger cp = new TestInteger();
		new Thread(cp.new Consumer()).start();
		new Thread(cp.new Producer()).start();

	}
}

如上所示,大体上是可以模拟取苹果和放苹果的过程的。这段代码可以正常运行,但是仔细瞧瞧这段代码其实是有问题的,其问题在于两个方面:

       1.如果有多个生产者,将会导致number的脏读写,会引起同步的问题。解决方法是加入线程同步机制。

       2.假设篮子已满或者篮子已空,此时,放苹果线程和取苹果线程依旧在占用CPU的调度资源,造成了资源浪费。解决方法是:篮子为空或者已满,让线程阻塞等待被唤醒

综上两个问题,改进给出程序片段二:

class Consumer implements Runnable {//消费者
		@Override
		public void run() {
			// TODO Auto-generated method stub
			while (true) {
				synchronized (number) {
					while (number <= 0) {
						try {
							number.wait();
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
					number--;
					number.notify();
					System.out.println("---------------------------------------------" + number);
				}
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}

		}

	}

	class Producer implements Runnable {//生产者

		@Override
		public void run() {
			// TODO Auto-generated method stub
			while (true) {
				synchronized (number) {
					while (number >= 5) {
						try {
							number.wait();
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
					number++;
					number.notify();
					System.out.println("++++++++++++++++++++++++++++++++++++++++++++++"	+ number);
				}
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
将上述的生产者和消费者代码更新后,发现编译运行出问题了: java.lang.IllegalMonitorStateException 

        这个错误在多线程编程之中经常会出现,这种错误的异常情况在《Java编程思想》703页有说明:“如果在非同步方法之中调用wait,notify等方法,程序可以通过编译,但是在运行时会报出IllegalMonitorStateException”,当前线程不是对象的拥有者。

       为什么会报这个错呢?number是外部类的对象,内部类可以直接访问,而且也加锁了。为什么还会报该异常呢?百思不得其解。

       然后,注意到一点特殊性,Integer与int之间存在自动的拆装箱的操作,由于拆装箱操作中Integer的对象会被新的对象替换掉。所以,其实在任何一个线程之中,如果做了修改操作,都会导致:在该线程(假设为A线程)的内部创建一个新的对象。按照对象创建的角度来讲,此时:调用新对象的wait或者notify方法时,线程并不持有新对象的对象锁,申请的对象锁是做运算之前的就对象,此时抛出异常就可以理解了。

      给出完整的代码如下:

public class TestSyncronized {
	private Apple apple;

	class Apple {
		/**
		 * 公共资源类
		 */
		private Integer number = 0;

		/**
		 * 增加公共资源
		 */
		public void increace() {
			number++;
			System.out.println("生产了一个,还剩:" + number);
		}

		/**
		 * 减少公共资源
		 */
		public void decreace() {
			number--;
			System.out.println("消费了一个,还剩:" + number);
		}
	}

	public TestSyncronized() {
		apple = new Apple();
	}


	class Consumer implements Runnable {//消费者
		@Override
		public void run() {
			// TODO Auto-generated method stub
			while (true) {
				synchronized (apple) {
					while(apple.number <= 0){
						try {
							System.out.println("等待中。。。。。。。。。。。。。");
							apple.wait();
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
					apple.decreace();
					apple.notify();
				}
				
			}

		}

	}

	class Producer implements Runnable {//生产者

		@Override
		public void run() {
			// TODO Auto-generated method stub
			while (true) {
				synchronized (apple) {
					while(apple.number >= 5){
						try {
							System.out.println("********************************************已满");
							apple.wait();
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
					apple.increace();
					apple.notify();
				}
				
			}
		}
	}

	public static void main(String[] args) {
		TestSyncronized cp = new TestSyncronized();
		new Thread(cp.new Consumer()).start();
		new Thread(cp.new Producer()).start();

	}
}
至此可以已经解决代码一中存在的两个问题。


附:

关于int和Integer自动拆装箱操作过程发生的细节:

写个简单的程序如下:

public class TestInteger {
	public static void main(String[] args) {
		Integer ii = 0;
		ii --;
		System.out.println(ii);
	}
}
编译后,通过javap -c 指令看起jvm执行指令:

public class TestInteger {

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       // 将int型的0推送至栈顶,这里的0即为我们定义的变量ii的字面值
       1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       // 静态方法调用:看注释,调用了Integer.valueOf方法。该函数返回了对应值为0的Integer对象
       4: astore_1
       // 将栈顶引用型数值存入第二个本地变量,即存入ii
       5: aload_1
       // 将第二个引用类型变量推送至栈顶   当前值:0
       6: astore_2
       // 将栈顶引用型数值存入第三个本地变量,变量名未知。 第三个变量值0
       7: aload_1
       // 将第二个引用类型变量推送至栈顶   当前值:Integer型0
       8: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
       // 函数调用,Integer.intValue(); 
      11: iconst_1 
      // 将int型的1推送至栈顶  
      12: isub
      // 将栈顶两个int数值相减并将结果压入栈顶  -1
      13: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      // 调用Integer.valueOf();
      16: dup
      // 复制栈顶数值并压入栈顶
      17: astore_1
      // 将栈顶引用型数值存入第二个本地变量,ii
      18: astore_3
      // 将栈顶引用型数值存入第四个本地变量
      19: aload_2
      // 将第三个引用类型变量推送至栈顶
      20: pop
      // 弹出栈顶元素
      21: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      // 打印
      24: aload_1
      // 将第二个引用类型变量推送至栈顶
      25: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      28: return
}

可以发现,过程之中装箱过程之中首先将i转化为Integer类型,后再将int型数取出,减一,然后将结果-1又自动装箱为一个新的Integer对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值