Java监视器支持两种线程:互斥和协作。
前面我们介绍了采用对象锁和重入锁来实现的互斥。这一篇中,我们来看一看线程的协作。
举个例子:有一家汉堡店举办吃汉堡比赛,决赛时有3个顾客来吃,3个厨师来做,一个服务员负责协调汉堡的数量。为了避免浪费,制作好的汉堡被放进一个能装有10个汉堡的长条状容器中,按照先进先出的原则取汉堡。如果容器被装满,则厨师停止做汉堡,如果顾客发现容器内的汉堡吃完了,就可以拍响容器上的闹铃,提醒厨师再做几个汉堡出来。此时服务员过来安抚顾客,让他等待。而一旦厨师的汉堡做出来,就会让服务员通知顾客,汉堡做好了,让顾客继续过来取汉堡。
这里,顾客其实就是我们所说的消费者,而厨师就是生产者。容器是决定厨师行为的监视器,而服务员则负责监视顾客的行为。
在JVM中,此种监视器被称为等待并唤醒监视器。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
在这种监视器中,一个已经持有该监视器的线程,可以通过调用监视对象的wait方法,暂停自身的执行,并释放监视器,自己进入一个等待区,直到监视器内的其他线程调用了监视对象的notify方法。当一个线程调用唤醒命令以后,它会持续持有监视器,直到它主动释放监视器。而这之后,等待线程会苏醒,其中的一个会重新获得监视器,判断条件状态,以便决定是否继续进入等待状态或者执行监视区域,或者退出。
请看下面的代码:
1
.
public
class
NotifyTest{
2
.
private
Stringflag
=
"
true
"
;
3
.
4
.
class
NotifyThread
extends
Thread{
5
.
public
NotifyThread(Stringname){
6
.
super
(name);
7
.}
8
.
public
void
run(){
9
.
try
{
10
.sleep(
3000
);
//
推迟3秒钟通知
11
.}
catch
(InterruptedExceptione){
12
.e.printStackTrace();
13
.}
14
.
15
.flag
=
"
false
"
;
16
.flag.notify();
17
.}
18
.};
19
.
20
.
class
WaitThread
extends
Thread{
21
.
public
WaitThread(Stringname){
22
.
super
(name);
23
.}
24
.
25
.
public
void
run(){
26
.
27
.
while
(flag
!=
"
false
"
){
28
.System.out.println(getName()
+
"
beginwaiting!
"
);
29
.
long
waitTime
=
System.currentTimeMillis();
30
.
try
{
31
.flag.wait();
32
.}
catch
(InterruptedExceptione){
33
.e.printStackTrace();
34
.}
35
.waitTime
=
System.currentTimeMillis()
-
waitTime;
36
.System.out.println(
"
waittime:
"
+
waitTime);
37
.}
38
.System.out.println(getName()
+
"
endwaiting!
"
);
39
.
40
.}
41
.}
42
.
43
.
public
static
void
main(String[]args)
throws
InterruptedException{
44
.System.out.println(
"
MainThreadRun!
"
);
45
.NotifyTesttest
=
new
NotifyTest();
46
.NotifyThreadnotifyThread
=
test.
new
NotifyThread(
"
notify01
"
);
47
.WaitThreadwaitThread01
=
test.
new
WaitThread(
"
waiter01
"
);
48
.WaitThreadwaitThread02
=
test.
new
WaitThread(
"
waiter02
"
);
49
.WaitThreadwaitThread03
=
test.
new
WaitThread(
"
waiter03
"
);
50
.notifyThread.start();
51
.waitThread01.start();
52
.waitThread02.start();
53
.waitThread03.start();
54
.}
55
.
56
.}
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
这段代码启动了三个简单的wait线程,当他们处于等待状态以后,试图由一个notify线程来唤醒。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
运行这段程序,你会发现,满屏的java.lang.IllegalMonitorStateException,根本不是你想要的结果。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
请注意以下几个事实:
1. 任何一个时刻,对象的控制权(monitor)只能被一个线程拥有。
2. 无论是执行对象的wait、notify还是notifyAll方法,必须保证当前运行的线程取得了该对象的控制权(monitor)。
3. 如果在没有控制权的线程里执行对象的以上三种方法,就会报java.lang.IllegalMonitorStateException异常。
4. JVM基于多线程,默认情况下不能保证运行时线程的时序性。
也就是说,当线程在调用某个对象的wait或者notify方法的时候,要先取得该对象的控制权,换句话说,就是进入这个对象的监视器。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
通过前面对同步的讨论,我们知道,要让一个线程进入某个对象的监视器,通常有三种方法:
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
1: 执行对象的某个同步实例方法
2: 执行对象对应的同步静态方法
3: 执行对该对象加同步锁的同步块
显然,在上面的例程中,我们用第三种方法比较合适。
于是我们将上面的wait和notify方法调用包在同步块中。
1
.
synchronized
(flag){
2
.flag
=
"
false
"
;
3
.flag.notify();
4
.}
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
1
.
synchronized
(flag){
2
.
while
(flag
!=
"
false
"
){
3
.System.out.println(getName()
+
"
beginwaiting!
"
);
4
.
long
waitTime
=
System.currentTimeMillis();
5
.
try
{
6
.flag.wait();
7
.}
catch
(InterruptedExceptione){
8
.e.printStackTrace();
9
.}
10
.waitTime
=
System.currentTimeMillis()
-
waitTime;
11
.System.out.println(
"
waittime:
"
+
waitTime);
12
.}
13
.System.out.println(getName()
+
"
endwaiting!
"
);
14
.}
但是,运行这个程序,我们发现事与愿违。那个非法监视器异常又出现了。。。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
我们注意到,针对flag的同步块中,我们实际上已经更改了flag对对象的引用: flag="false";
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
显然,这样一来,同步块也无能为力了,因为我们根本不是针对唯一的一个对象在进行同步。
我们不妨将flag封装到JavaBean或者数组中去,这样用JavaBean对象或者数组对象进行同步,就可以达到既能修改里面参数又不耽误同步的目的。
1
.
private
Stringflag[]
=
{
"
true
"
};
1
.
synchronized
(flag){
2
.flag[
0
]
=
"
false
"
;
3
.flag.notify();
4
.}
1
.
synchronized
(flag){
2
.flag[
0
]
=
"
false
"
;
3
.flag.notify();
4
.}
synchronized
(flag){
5
.
while
(flag[
0
]
!=
"
false
"
){
6
.System.out.println(getName()
+
"
beginwaiting!
"
);
7
.
long
waitTime
=
System.currentTimeMillis();
8
.
try
{
9
.flag.wait();
10
.
11
.}
catch
(InterruptedExceptione){
12
.e.printStackTrace();
13
.}
运行这个程序,看不到异常了。但是仔细观察结果,貌似只有一个线程被唤醒。利用jconsole等工具查看线程状态,发现的确还是有两个线程被阻塞的。这是为啥呢?
程序中使用了flag.notify()方法。只能是随机的唤醒一个线程。我们可以改用flag.notifyAll()方法。这样,所有被阻塞的线程都会被唤醒了。
最终代码请读者自己修改,这里不再赘述。
好了,亲爱的读者们,让我们回到开篇提到的汉堡店大赛问题当中去,来看一看厨师、服务生和顾客是怎么协作进行这个比赛的。
首先我们构造故事中的三个次要对象:汉堡包、存放汉堡包的容器、服务生
public
class
Waiter{
//
服务生,这是个配角,不需要属性。
}
class
Hamberg{
//
汉堡包
private
int
id;
//
汉堡编号
private
Stringcookerid;
//
厨师编号
public
Hamberg(
int
id,Stringcookerid){
this
.id
=
id;
this
.cookerid
=
cookerid;
System.out.println(
this
.toString()
+
"
wasmade!
"
);
}
@Override
public
StringtoString(){
return
"
#
"
+
id
+
"
by
"
+
cookerid;
}
}
class
HambergFifo{
//
汉堡包容器
List
<
Hamberg
>
hambergs
=
new
ArrayList
<
Hamberg
>
();
//
借助ArrayList来存放汉堡包
int
maxSize
=
10
;
//
指定容器容量
//
放入汉堡
public
<
T
extends
Hamberg
>
void
push(Tt){
hambergs.add(t);
}
//
取出汉堡
public
Hambergpop(){
Hambergh
=
hambergs.get(
0
);
hambergs.remove(
0
);
return
h;
}
//
判断容器是否为空
public
boolean
isEmpty(){
return
hambergs.isEmpty();
}
//
判断容器内汉堡的个数
public
int
size(){
return
hambergs.size();
}
//
返回容器的最大容量
public
int
getMaxSize(){
return
this
.maxSize;
}
}
接下来我们构造厨师对象:
class
Cooker
implements
Runnable{
//
厨师要面对容器
HambergFifopool;
//
还要面对服务生
Waiterwaiter;
public
Cooker(Waiterwaiter,HambergFifohambergStack){
this
.pool
=
hambergStack;
this
.waiter
=
waiter;
}
//
制造汉堡
public
void
makeHamberg(){
//
制造的个数
int
madeCount
=
0
;
//
因为容器满,被迫等待的次数
int
fullFiredCount
=
0
;
try
{
while
(
true
){
//
制作汉堡前的准备工作
Thread.sleep(
1000
);
if
(pool.size()
<
pool.getMaxSize()){
synchronized
(waiter){
//
容器未满,制作汉堡,并放入容器。
pool.push(
new
Hamberg(
++
madeCount,Thread.currentThread().getName()));
//
说出容器内汉堡数量
System.out.println(Thread.currentThread().getName()
+
"
:Thereare
"
+
pool.size()
+
"
Hambergsinall
"
);
//
让服务生通知顾客,有汉堡可以吃了
waiter.notifyAll();
System.out.println(
"
###Cooker:waiter.notifyAll():"+
" Hi!Customers,wegotsomenewHambergs!
"
);
}
}
else
{
synchronized
(pool){
if
(fullFiredCount
++
<
10
){
//
发现容器满了,停止做汉堡的尝试。
System.out.println(Thread.currentThread().getName()
+
"
:HambergPoolisFull,Stopmakinghamberg
"
);
System.out.println(
"
###Cooker:pool.wait()
"
);
//
汉堡容器的状况使厨师等待
pool.wait();
}
else
{
return
;
}
}
}
//
做完汉堡要进行收尾工作,为下一次的制作做准备。
Thread.sleep(
1000
);
}
}
catch
(Exceptione){
madeCount
--
;
e.printStackTrace();
}
}
public
void
run(){
makeHamberg();
}
}
接下来,我们构造顾客对象:
class
Customer
implements
Runnable{
//
顾客要面对服务生
Waiterwaiter;
//
也要面对汉堡包容器
HambergFifopool;
//
想要记下自己吃了多少汉堡
int
ateCount
=
0
;
//
吃每个汉堡的时间不尽相同
long
sleeptime;
//
用于产生随机数
Randomr
=
new
Random();
public
Customer(Waiterwaiter,HambergFifopool){
this
.waiter
=
waiter;
this
.pool
=
pool;
}
public
void
run(){
while
(
true
){
try
{
//
取汉堡
getHamberg();
//
吃汉堡
eatHamberg();
}
catch
(Exceptione){
synchronized
(waiter){
System.out.println(e.getMessage());
//
若取不到汉堡,要和服务生打交道
try
{
System.out.println(
"
###Customer:waiter.wait():"+
" Sorry,Sir,thereisnohambergsleft,pleasewait!
"
);
System.out.println(Thread.currentThread().getName()
+
"
:OK,Waitingfornewhambergs
"
);
//
服务生安抚顾客,让他等待。
waiter.wait();
continue
;
}
catch
(InterruptedExceptionex){
ex.printStackTrace();
}
}
}
}
}
private
void
eatHamberg(){
try
{
//
吃每个汉堡的时间不等
sleeptime
=
Math.abs(r.nextInt(
3000
))
*
5
;
System.out.println(Thread.currentThread().getName()
+
"
:I'meatingthehambergfor
"
+
sleeptime
+
"
milliseconds
"
);
Thread.sleep(sleeptime);
}
catch
(Exceptione){
e.printStackTrace();
}
}
private
void
getHamberg()
throws
Exception{
Hamberghamberg
=
null
;
synchronized
(pool){
try
{
//
在容器内取汉堡
hamberg
=
pool.pop();
ateCount
++
;
System.out.println(Thread.currentThread().getName()
+
"
:IGot
"
+
ateCount
+
"
Hamberg
"
+
hamberg);
System.out.println(Thread.currentThread().getName()
+
"
:Therearestill
"
+
pool.size()
+
"
hambergsleft
"
);
}
catch
(Exceptione){
pool.notifyAll();
System.out.println(
"
###Customer:pool.notifyAll()
"
);
throw
new
Exception(Thread.currentThread().getName()
+
"
:OHMYGOD!!!!Nohambergsleft,Waiter![Ringthebellbesidesthehambergpool]
"
);
}
}
}
}
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
最后,我们构造汉堡店,让这个故事发生:
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
public
class
HambergShop{
Waiterwaiter
=
new
Waiter();
HambergFifohambergPool
=
new
HambergFifo();
Customerc1
=
new
Customer(waiter,hambergPool);
Customerc2
=
new
Customer(waiter,hambergPool);
Customerc3
=
new
Customer(waiter,hambergPool);
Cookercooker
=
new
Cooker(waiter,hambergPool);
public
static
void
main(String[]args){
HambergShophambergShop
=
new
HambergShop();
Threadt1
=
new
Thread(hambergShop.c1,
"
Customer1
"
);
Threadt2
=
new
Thread(hambergShop.c2,
"
Customer2
"
);
Threadt3
=
new
Thread(hambergShop.c3,
"
Customer3
"
);
Threadt4
=
new
Thread(hambergShop.cooker,
"
Cooker1
"
);
Threadt5
=
new
Thread(hambergShop.cooker,
"
Cooker2
"
);
Threadt6
=
new
Thread(hambergShop.cooker,
"
Cooker3
"
);
t4.start();
t5.start();
t6.start();
try
{
Thread.sleep(
10000
);
}
catch
(Exceptione){
}
t1.start();
t2.start();
t3.start();
}
}
运行这个程序吧,然后你会看到我们汉堡店的比赛进行的很好,只是不
知道那些顾客是不是会被撑到。。。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
读到这里,有的读者可能会想到前面介绍的重入锁ReentrantLock。
有的读者会问:如果我用ReentrantLock来代替上面这些例程当中的 synchronized块,是不是也可以呢?感兴趣的读者不妨一试。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
但是在这里,我想提前给出结论,就是,
如果用ReentrantLock的lock()和unlock()方法代替上面的synchronized块,那么上面这些程序还是要抛出 java.lang.IllegalMonitorStateException异常的,不仅如此,你甚至还会看到线程死锁。原因就是当某个线程调用第三方对象的wait或者notify方法的时候,并没有进入第三方对象的监视器,于是抛出了异常信息。但此时,程序流程如果没有用finally来处理 unlock方法,那么你的线程已经被lock方法上锁,并且无法解锁。程序在java.util.concurrent框架的语义级别死锁了,你用 JConsole这种工具来检测JVM死锁,还检测不出来。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
正确的做法就是,只使用ReentrantLock,而不使用wait或者notify方法。因为ReentrantLock已经对这种互斥和协作进行了概括。所以,根据你程序的需要,请单独采用重入锁或者synchronized一种同步机制,最好不要混用。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
好了,我们现在明白:
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
1. 线程的等待或者唤醒,并不是让线程调用自己的wait或者notify方法,而是通过调用线程共享对象的wait或者notify方法来实现。
2. 线程要调用某个对象的wait或者notify方法,必须先取得该对象的监视器。
3. 线程的协作必须以线程的互斥为前提,这种协作实际上是一种互斥下的协作