线程的协调
在Java多线程中,对于线程之间共享的实例资源,可以通过synchronized修饰符修饰其方法,实现线程之间的同步。另外,在多线程设计中,还需考虑到线程之间的协调。关于协调的一个典型设计模式便是Producer-Consumer(生产者-消费者)模式。
Producer-Consumer(生产者-消费者)模式
在这一模式中,存在Producer和Consumer两类线程,Producer线程用于生成Data(共享数据资源),而Consumer线程用于消费Data,在Producer和Consumer之间,存在对于Data生成和消费的协调,即当不存在Data时,Consumer线程需要等待Producer线程生成新的Data,而当Data过多时,Producer线程需要等待Consumer线程消费过多的Data。在Producer-Consumer模式中,引入了Channel(管道)类,负责Data在各线程之间的协调。Producer-Consumer模式的UML类图如下所示。
在上图中,Producer线程类和Consumer线程类均包含对Channel类对象的引用,而Channel类对象封装了Data类,并分别实现了生产和消费Data的同步方法produce和consume。在produce和consume方法中,通过使用wait和notifyAll方法进一步实现Data的协调。
wait、notify、notifyAll
在Java中,wait、notify、notifyAll是Object类的方法,用于实现对调用对象方法的线程的暂停和唤醒。wait用于暂停线程,将其放入对象的wait set(线程等待集合),而notify、notifyAll方法用于唤醒wait set中的线程,使其继续执行。notify和notifyAll的不同是,当wait set中存在多个线程时,notify只会从中随机唤醒一个线程,而notifyAll会从中唤醒所有线程,由其进行竞争,获得同步锁并继续执行。
一个关于wait、notify、notifyAll使用的简单示例如图所示。
Producer-Consumer(生产者-消费者)模式的实现示例
Producer和Consumer线程类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
27package com.wt.pc;
public class Producer extends Thread{
//对Channel对象的引用
Channel channel = null;
//Consumer类构造函数
public Producer(String producerName,Channel channel)
{
super(producerName);
this.channel = channel;
}
//Producer线程每隔500ms尝试生成新的data
public void run()
{
try{
while(true)
{
channel.produce(Integer.toString(Channel.dataId++));
Thread.sleep(500);
}
}catch(Exception e){}
}
}
1 | package com.wt.pc; |
Channel和Data类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
59package com.wt.pc;
public class Channel {
//静态变量,用于生成data名称
static int dataId = 0;
//存储data的数组
Data dataList[];
//当前未消费data的头序号
int head;
//当前未消费data的尾序号的下一个
int tail;
//当前未消费data的数目
int count;
//Channel类构造函数
//数组容量为3,其他值初始化为0
public Channel()
{
dataList = new Data[3];
head = 0;
tail = 0;
count = 0;
}
//produce方法,用于生成data
public synchronized void produce(String dataName)throws Exception{
//当数组容量已满时,即不能再生成新data时,当前线程进入wait set
while (count>=3)
{
wait();
}
//生成新的data
System.out.println(Thread.currentThread().getName()+" is producing "+dataName);
dataList[tail] = new Data();
dataList[tail].setDataName(dataName);
tail = (tail+1)%3;
count++;
Thread.sleep(400);
//唤醒wait set中的线程
notifyAll();
}
//consume方法,用于消费data
public synchronized void consume()throws Exception{
//当数组容量为空时,即不能再消费data时,当前线程进入wait set
while (count<=0)
{
wait();
}
//消费data
System.out.println(Thread.currentThread().getName()+" is consuming "+dataList[head].getDataName());
head = (head+1)%3;
count--;
Thread.sleep(300);
//唤醒wait set中的线程
notifyAll();
}
}
1 | package com.wt.pc; |
主函数1
2
3
4
5
6
7
8
9
10
11public static void main(String args[])
{
Channel channel = new Channel();
new Producer("p1",channel).start();
new Producer("p2",channel).start();
new Producer("p3",channel).start();
new Consumer("c1",channel).start();
new Consumer("c2",channel).start();
new Consumer("c3",channel).start();
}
执行结果
p2 is producing 0
c2 is consuming 0
p3 is producing 2
p1 is producing 1
c3 is consuming 2
c1 is consuming 1
p1 is producing 5
p3 is producing 4
c2 is consuming 5
p2 is producing 3
p3 is producing 7
c1 is consuming 4
c3 is consuming 3
p3 is producing 9
p1 is producing 6
c2 is consuming 7
p2 is producing 8
c3 is consuming 9
c1 is consuming 6
p2 is producing 12
p3 is producing 10
c2 is consuming 8
……
从执行结果中可以看出,生成Data线程的执行次数p和消费Data线程的执行次数c始终满足:
p>=c且p<=c+3
即保证Data数组在消费时存在Data但也不超过数组容量,从而有效实现Producer和Consumer线程类关于Data的协调。
总结
在Java多线程设计中,需要充分考虑线程之间的同步和协调。针对不同的应用场景,可以采用不同的设计模式,已有的设计模式有Single Threaded Execution、Immutable、Guarded Suspension、Balking、Producer-Consumer、Read-Write Lock、Thread-Per-Message、Worker Thread等,具体可进一步参考网上有关“Java多线程设计模式”的教程。