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

目录
  1. 问题描述:
  2. 问题思路:
  3. 附:

问题描述:

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

问题思路:

  线程同步的经典问题——生产者和消费者,只不过换了个皮囊。此处只有一个生产者和一个消费者。
  初步实现,不罗嗦,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
}
}
}
}
class Producer implements Runnable {//生产者
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
while (number < 5) {
number++;
System.out.println("++++++"+ number);
}
}
}
}
}

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

  • 如果有多个生产者,将会导致number的脏读写,会引起同步的问题。解决方法是加入线程同步机制。
  • 假设篮子已满或者篮子已空,此时,放苹果线程和取苹果线程依旧在占用CPU的调度资源,造成了资源浪费。解决方法是:篮子为空或者已满,让线程阻塞等待被唤醒
      综上两个问题,改进给出程序片段二:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    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方法时,线程并不持有新对象的对象锁,申请的对象锁是做运算之前的就对象,此时抛出异常就可以理解了。
给出完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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();
}
}
}
}
}

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

附:

  关于int和Integer自动拆装箱操作过程发生的细节:
  写个简单的程序如下:

1
2
3
4
5
6
7
public class TestInteger {
public static void main(String[] args) {
Integer ii = 0;
ii --;
System.out.println(ii);
}
}

编译后,通过javap -c 指令看起jvm执行指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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对象。