问题描述:
模拟实现一种场景:有一个篮子,最多可以装5个苹果,一个人从篮子中取苹果,一个人向篮子中放苹果。
问题思路:
线程同步的经典问题——生产者和消费者,只不过换了个皮囊。此处只有一个生产者和一个消费者。
初步实现,不罗嗦,直接上代码:
1 | public class TestInteger { |
如上所示,大体上是可以模拟取苹果和放苹果的过程的。这段代码可以正常运行,但是仔细瞧瞧这段代码其实是有问题的,其问题在于两个方面:
- 如果有多个生产者,将会导致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
54class 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 | public class TestSyncronized { |
至此可以已经解决代码一中存在的两个问题。
附:
关于int和Integer自动拆装箱操作过程发生的细节:
写个简单的程序如下:
1 | public class TestInteger { |
编译后,通过javap -c 指令看起jvm执行指令:
1 | public class TestInteger { |
可以发现,过程之中装箱过程之中首先将i转化为Integer类型,后再将int型数取出,减一,然后将结果-1又自动装箱为一个新的Integer对象。