多线程程序的评量标准
-
安全性:不损坏对象
不安全是指,对象的状态处于非预期状态,比如账户余额变成了负值 -
生存性:进行必要的处理
生存性是指:程序能正常运行,可进行必要的处理,影响生存性的典型问题有出现死锁 -
复用性:可再利用类
复用性是指代码重用,若复用性好,可减少大量重复代码 -
性能:能快速,大量进行处理
性能有两个方面的考虑因素:吞吐量和响应性,客户端程序比较重视响应性,服务端程序更重视吞吐量,吞吐量是指单位时间内完成的任务,响应性是指提交任务后多长时间内能收到程序的反馈。比如说我们在QQ时,经常感觉QQ卡,这便是响应性问题。
其中安全性和生存性是必要的,如果安全性和生存性都没有保证,就无所谓别的考量了。复用性和性能决定了程序的质量
《多线程设计模式》一共讲了12个设计模式,列举如下。
1. Single Threaded Execution
只允许单个线程执行对象的某个方法,以保护对象的多个状态。
实现时需用synchronized
修饰引用受保护的状态的方法,这样就只能有单个线程访问该方法,其它线程由于不能获取锁而等待,因为只有一个线程去访问受保护状态变量,故此不需要担心该状态变量被别的线程修改。
也可以用synchronized
修饰代码块来保护状态字段。
示例程序:
public class Gate {
private String _name = "NoBody";
private String _where = "NoBody";
public synchronized void pass(String name, String where) {
_name = name;
_where = where;
check();
}
private void check() {
if (_name.charAt(0) != _where.charAt(0)) {
System.out.println("*****************Broken**************");
}
}
}
如果这里不用synchronized
修饰pass
方法,多线程环境下会有多个线程同时执行pass
方法,容易造成状态不一致,引入安全性问题。
适用场景:
多线程环境下如果状态变量(可能有多个状态变量,并且它们之间是相关的)被多个线程访问,并且可能会发生变化,此时需要将状态变量封装起来(可以用类进行封装),并将访问这些状态变量的方法用synchronized
进行保护。可以用synchronized
修饰方法,也可以修饰代码块。
注意事项:
一定要注意synchronized
是通过获取哪个锁来保护状态变量,如果保护状态变量时使用不同的锁对象,那么多个线程仍然可以同时访问被保护的状态变量,尤其是保护多个相关状态变量时一定要记得用同一个锁对象。synchronized
修饰方法时,获取的锁对象是synchronied
方法所在类的实例,synchorized
修饰this
时,获取的锁对象也是当前类的实例。
synchronized
修饰符不会被继承,也就是说我们覆盖父类的synchronized
方法时,如果不添加synchronized
修饰符,就不能保护状态变量,因此覆盖父类方法时,如果想保护某些状态变量,记得添加synchronized
修饰符。
2. Immutable
在single threaded executetion
这个模式里我们使用了synchronized
来保护需要保护的状态变量,因为这些状态可能会变化,如果不保护的话,可能会破坏对象。但是用synchronized
保护变量也带来了性能问题,因为获取锁需要时间,并且如果多个线程竞争锁的话,会让某些线程进入这个锁的条件队列,暂停执行,这样会降低性能。
如果状态根本不会发生变化,就不需要用锁保护,这就是Immutable
模式。
示例程序:
public final class Person {
private final String _name;
private final String _address;
public Person(String name, String address) {
_name = name;
_address = address;
}
public String getName() {
return _name;
}
public String getAddress() {
return _address;
}
@Override
public String toString() {
return "Person [_name=" + _name + ", _address=" + _address + "]";
}
}
Person
类用final
修饰,防止被继承。
_name
和_address
都用final
修饰,防止被修改,只能在定义时初始化,或者在构造器里初始化,Person
类也只提供了对这些状态字段的get
方法,故此外界调用该类的实例时无法修改这些状态。
适用场景:
对于那些不会变化的状态可用Immutable
类进行封装,这样可避免用锁同步,从而提高性能。
注意事项:
String
就是一个Immutable
类,与之相对应的StringBuilder
或者StringBuffer
是muttable类。我们在设计类时,针对那些需要共享并且访问很频繁的实例,可将其设置为Immutalbe
类,如果在少数情况下它的状态也可能会变化,可为之设计相对应的muttable类,像String
和StringBuffer
的关系一样。
StringBuilder
是非线程安全的,StringBuffer
是线程安全的,String
也是线程安全的,因为它是immutable类。
java里的包装器类全是immutable类。
3. Guarded Suspension
当我们调用对象某个的某个方法时,可能对象当前状态并不满足执行的条件,于是需要等待,这就是Guarded Suspension
模式。只有当警戒条件满足时,才执行,否则等待,另外对象必须有改变其状态的方法。
示例程序:
public class RequestQueue {
private final LinkedList<Request> _queue = new LinkedList<Request>();
public synchronized Request getRequest() {
while (_queue.size() <= 0) {
try {
wait();
} catch (InterruptedException e) {
}
}
return _queue.removeFirst();
}
public synchronized void putRequest(Request request) {
_queue.add(request);
notifyAll();
}
}
_queue.size()>0
便是警戒条件,只有当_queue.size()>0
才能调用_queue.removeFirst()
,当警戒条件不满足时,需要wait
。
putRequest
方法可以改变RequestQueue
的状态,使getRequest
方法里的警戒条件满足。
适用场景:
某个调用者的方法在执行时如果希望当状态不满足时等待状态满足后再执行,如果状态满足,则立即执行,可考虑使用Guarded Suspension
模式。
注意事项:
Guarded Suspension
里的警戒方法(等待状态成立才执行的方法)是同步阻塞的,状态不满足时,调用该方法的线程会阻塞。
Guarded Suspension
里的状态变更方法里须记得在状态变更后,调用notifyAll
,使得调用警戒方法的线程可恢复执行。
4. Balking
Balking
模式与Guarded Suspension
模式相似,都是在对象状态不符合要求时需要进行一些处理,不过Guared Suspension
在状态不满足要求时,会等待并阻塞线程,而Balking
模式是直接返回,并不等待。调用者可暂时先做别的工作,稍后再来调用该对象的方法。
示例程序:
public class Data {
private final String _file_name;
private String _content;
private boolean _changed;
public Data(String filename, String conetent) {
_file_name = filename;
_content = conetent;
_changed = false;
}
public synchronized void change(String newContent) {
_content = newContent;
_changed = true;
}
public synchronized void save() throws IOException {
if (!_changed)
return;
doSave();
_changed = false;
}
private void doSave() throws IOException {
Writer writer = new FileWriter(_file_name);
writer.write(_content);
writer.close();
}
}
save
方法里首先检测字符串是否有变化,如果没有变化则立即返回,否则才保存字符串,这样可避免不必要的IO,提高性能。
上述实例中的警戒条件是_changed
为true
适用场景:
不想等待警戒条件成立时,适合使用Balking
模式。
警戒条件只有第一次成立时,适合使用Balking
模式。
注意事项:
该模式并不会等待警戒条件成立,当警戒条件不成立时直接返回了,故此改变状态的方法也就不需要调用notifyAll
方法。
另外注意不管是警戒条件方法还是改变状态的方法都需要用synchronized
同步,因为这里封装了多个数据,一个用于判断警戒条件的状态,还有真实数据。
5. Producer-Consumer
生产者消费者问题是操作系统里非常经典的同步问题,生产者生产好数据后,放到缓冲区,消费者从缓冲区取出数据。但是当缓冲区满了的时候,生产者不可再将生产好的数据放到缓冲区,当缓冲区没有数据的时候消费者不可再从缓冲区里取出数据。
解决生产者消费者问题的方案称之为生产者消费者模式,在该模式里可能有多个生产者,多个消费者,生产者和消费者都有独立的线程。其中最关键的是放置数据的缓冲区,生产者和消费者在操作缓冲区时都必须同步,生产者往缓冲区放置数据时,如果发现缓冲区已满则等待,消费者从缓冲区取数据时如果发现缓冲区没有数据,也必须等待。
示例程序:
public class Table {
private final String[] _buffer;
private int _tail;
private int _head;
private int _count;
public Table(int count) {
_buffer = new String[count];
_head = 0;
_tail = 0;
_count = 0;
}
public synchronized void put(String cake) throws InterruptedException {
while (_count >= _buffer.length) {
wait();
}
_buffer[_tail] = cake;
_tail = (_tail + 1) % _count;
_count++;
notifyAll();
}
public synchronized String take() throws InterruptedException {
while (_count <= 0) {
wait();
}
String cake = _buffer[_head];
_head = (_head + 1) % _count;
_count--;
notifyAll();
return cake;
}
}
这里table
扮演的便是数据缓冲区的角色,当消费者调用take
取数据时,如果发现数据数目少于0时,便会等待,当生产者调用put
放数据时,如果发现数据数目大于缓冲区大小时,也会等待。
适用场景:
当程序里有多个生产者角色或者多个消费者角色操作同一个共享数据时,适合用生产者消费者模式。比如下载模块,通常会有多个下载任务线程(消费者角色),用户点击下载按钮时产生下载任务(生产者角色),它们会共享任务队列。
注意事项:
不管是生产方法还是消费方法,当警戒条件不满足时,一定要等待,警戒条件满足后执行完放置数据逻辑或者取出数据逻辑后一定要调用notifyAll
方法,使得其它线程恢复运行。
6. Read-Write Lock
先前的几个多线程设计模式里,操作共享数据时,不管如何操作数据一律采取互斥的策略(除了Immutable
模式),即只允许一个线程执行同步方法,其它线程在共享数据的条件队列里等待,只有执行同步方法的线程执行完同步方法后被阻塞的线程才可在获得同步锁后继续执行。
这样效率其实有点低,因为读操作和读操作之间并不需要互斥,两个读线程可以同时操作共享数据,读线程和写线程同时操作共享数据会有冲突,两个写线程同时操作数据也会有冲突。
示例程序:
Data
类
public class Data {
private final char[] _buffer;
private final ReadWriteLock _lock = new ReadWriteLock();
public Data(int size) {
_buffer = new char[size];
for (int i = 0; i < size; i++)
_buffer[i] = '*';
}
public char[] read() throws InterruptedException {
_lock.readLock();
try {
return doRead();
} finally {
_lock.readUnlock();
}
}
public void write(char c) throws InterruptedException {
_lock.writeLock();
try {
doWrite(c);
} finally {
_lock.writeUnock();
}
}
private char[] doRead() {
char[] newbuf = new char[_buffer.length];
for (int i = 0; i < newbuf.length; i++)
newbuf[i] = _buffer[i];
slowly();
return newbuf;
}
private void doWrite(char c) {
for (int i = 0; i < _buffer.length; i++) {
_buffer[i] = c;
slowly();
}
}
private void slowly() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ReadWriteLock
类
public class ReadWriteLock {
private int _reading_readers = 0;
private int _waiting_writers = 0;
private int _writing_writers = 0;
private boolean _prefer_writer = true;
public synchronized void readLock() throws InterruptedException {
while (_writing_writers > 0 || (_prefer_writer && _waiting_writers > 0)) {
wait();
}
_reading_readers++;
}
public synchronized void readUnlock() {
_reading_readers--;
_prefer_writer = true;
notifyAll();
}
public synchronized void writeLock() throws InterruptedException {
_waiting_writers++;
try {
while (_reading_readers > 0 || _writing_writers > 0)
wait();
} finally {
_waiting_writers--;
}
_writing_writers++;
}
public synchronized void writeUnock() {
_writing_writers--;
_prefer_writer = false;
notifyAll();
}
}
这里为读写锁设置了单独的类ReadWriteLock
,ReadWriteLock
提供了4个方法readLock
,readUnlock
,writeLock
,writeUnlock
。
读线程在读取共享数据时,先调用readLock
方法获取读锁,然后使用try
块读取共享数据并在finnally
块中调用readUnlock
释放读锁。写线程在写入共享数据时,先调用writeLock
方法获取写锁,然后使用try
块写入共享数据并在finnally
块中调用writeUnlock
方法释放写锁。
实现ReadWriteLock
时使用了_waiting_writers
和_prefer_writer
,其实如果不采用这两个字段也能实现读写锁,但是使用了_prefer_writer
后可以让读线程以及写线程不致于饥饿。每次读线程调用完readUnlock
后设置_prefer_writer
为true
,此时如果有写线程等待写入,便可恢复执行,而不是由其它读线程继续执行。每次写线程调用完writeUnlock
后,_prefer_writer
为false
,此时等待读取的线程可恢复执行。
适用场景:
操作共享数据的读线程明显多于写线程时可采用读写锁模式提高程序性能。
注意事项:
Java 5的concurrent包里已经有ReadWriteLock
接口,对应的类有ReentrantReadWriteLock
,没必要自己实现ReadWriteLock
类。并发库里的类都是经过测试的稳定的类,并且性能也会比自己写的类要高,因此我们应该优先选择并发库里的类。
7. Thread-Per-Message
实现某个方法时创建新线程去完成任务,而不是在本方法里完成任务,这样可提高响应性,因为有些任务比较耗时。
示例程序:
public class Host {
private final Handler _handler=new Handler();
public void request(final int count, final char c){
new Thread(){
public void run(){
_handler.handle(count, c);
}
}.start();
}
}
实现Host
类的方法时,新建了一个线程调用Handler
对象处理request
请求。
每次调用Host
对象的request
方法时都会创建并启动新线程,这些新线程的启动顺序不是确定的。
适用场景:
适合在操作顺序无所谓时使用,因为请求的方法里新建的线程的启动顺序不是确定的。
在不需要返回值的时候才能使用,因为request
方法不会等待线程结束才返回,而是会立即返回,这样得不到请求处理后的结果。
注意事项:
每次调用都会创建并启动一个新线程,对新建线程没有控制权,实际应用中只有很简单的请求才会用Thread-Per-Message
这个模式,因为通常我们会关注返回结果,也会控制创建的线程数量,否则系统会吃不消。
8. Worker Thread
在Thread-Per-Message
模式里,每次函数调用都会启动一个新线程,但是启动新线程的操作其实是比较繁重的,需要比较多时间,系统对创建的线程数量也会有限制。我们可以预先启动一定数量的线程,组成线程池,每次函数调用时新建一个任务放到任务池,预先启动的线程从任务池里取出任务并执行。这样便可以控制线程的数量,也避免了每次启动新线程的高昂代价,实现了资源重复利用。
示例程序:
Channel
类:
public class Channel {
private static final int MAX_REQUEST = 100;
private final Request[] _request_queue;
private int tail;
private int head;
private int count;
private WorkerThread[] _thread_pool;
public Channel(int threads) {
_request_queue = new Request[MAX_REQUEST];
tail = 0;
head = 0;
count = 0;
_thread_pool = new WorkerThread[threads];
for (int i = 0; i < threads; i++) {
_thread_pool[i] = new WorkerThread("Worker-" + i, this);
}
}
public void startWorkers() {
for (int i = 0; i < _thread_pool.length; i++)
_thread_pool[i].start();
}
public synchronized Request takeRequest()
throws InterruptedException {
while (count <= 0) {
wait();
}
Request request = _request_queue[head];
head = (head + 1) % _request_queue.length;
count--;
notifyAll();
return request;
}
public synchronized void putRequest(Request request)
throws InterruptedException {
while (count >= _request_queue.length) {
wait();
}
_request_queue[tail] = request;
tail = (tail + 1) % _request_queue.length;
count++;
notifyAll();
}
}
WorkerThread
类:
public class WorkerThread extends Thread {
private final Channel _channel;
public WorkerThread(String name, Channel channel) {
super(name);
_channel = channel;
}
@Override
public void run() {
while (true) {
Request request;
try {
request = _channel.takeRequest();
request.execute();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
channel
类集成了线程池和任务池,对外提供了startWorkers
方法,外界可调用该方法启动所有工作线程,然后通过putRequest
方法向任务池添加任务,工作者线程会自动从任务池里取出任务并执行。
适用场景:
和Thread-Per-Message
模式一样,Worker Thread
模式实现了invocation
和exectution
的分离,即调用和执行分离,调用者调用方法运行在一个线程,任务的执行在另一个线程。调用者调用方法后可立即返回,提高了程序的响应性。另外也正是因为调用和执行分离了,我们可以控制任务的执行顺序,还可以取消任务,还能分散处理,将任务交给不同的机器执行,如果没有将调用和执行分离,这些特性是无法实现的。
适合有大量任务并且还需要将任务执行分离的程序,比如象应用分发类App,需要经常和服务器通信获取数据,并且通信消息可能还有优先级。
注意事项:
注意控制工作者线程的数量,如果过多,那么会有不少工作者线程并没有工作,会浪费系统资源,如果过少会使得任务池里塞满,导致其它线程长期阻塞。可根据实际工作调整线程数量,和任务池里的最大任务池数。
如果worker thread
只有一条,工人线程处理的范围就变成单线程了,可以省去共享互斥的必要。通常GUI框架都是这么实现的,操作界面的线程只有一个,界面元素的方法不需要进行共享互斥。如果操作界面的线程有多个,那么必须进行共享互斥,我们还会经常设计界面元素的子类,子类实现覆盖方法时也必须使用synchronized
进行共享互斥,引入共享互斥后会引入锁同步的开销,使程序性能降低,并且如果有不恰当的获取锁的顺序,很容易造成死锁,这使得GUI程序设计非常复杂,故此GUI框架一般都采用单线程。
Java 5的并发包里已经有线程池相关的类,无需自己实现线程池。可使用Executors
的方法启动线程池,这些方法包括newFixedThreadPool
,newSingleThreadExecutor
,newCachedThreadPool
,newScheduledThreadPool
等等。
9. Future
在Thread-Per-Message
模式和Worker Thread
模式里,我们实现了调用和执行分离。但是通常我们调用一个函数是可以获得返回值的,在上述两种模式里,虽然实现了调用和执行相分离,但是并不能获取调用执行的返回结果。Future
模式则可以获得执行结果,在调用时返回一个Future
,可以通过Future
获得真正的执行结果。
示例程序:
Host
类
public class Host {
public Data request(final int count, final char c) {
System.out.println(" request (" + count
+ ", " + c + " ) BEGIN");
final FutureData future = new FutureData();
new Thread() {
@Override
public void run() {
RealData realData = new RealData(count, c);
future.setRealData(realData);
}
}.start();
return future;
}
}
Data
接口
public interface Data {
public String getContent();
}
FutureData
类
public class FutureData implements Data {
private boolean _ready = false;
private RealData _real_data = null;
public synchronized void setRealData(RealData realData) {
if (_ready)
return;
_real_data = realData;
_ready = true;
notifyAll();
}
@Override
public synchronized String getContent() {
while (!_ready) {
try {
wait();
} catch (InterruptedException e) {
}
}
return _real_data.getContent();
}
}
RealData
类
public class RealData implements Data {
private final String _content;
public RealData(int count, char c) {
System.out.println("Making Realdata("
+ count + "," + c + ") BEGIN");
char[] buffer = new char[count];
for (int i = 0; i < count; i++) {
buffer[i] = c;
try {
Thread.sleep(100);
} catch (Exception e) {
}
}
System.out.println(" making Real Data("
+ count + "," + c + ") END");
_content = new String(buffer);
}
@Override
public String getContent() {
return _content;
}
}
适用场景:
如果既想实现调用和执行分离,又想获取执行结果,适合使用Future
模式。
Future
模式可以获得异步方法调用的”返回值”,分离了”准备返回值”和”使用返回值”这两个过程。
注意事项:
Java 5并发包里已经有Future
接口,不仅能获得返回结果,还能取消任务执行。当调用ExecutorService
对象的submit
方法向任务池提交一个Callable
任务后,可获得一个Future
对象,用于获取任务执行结果,并可取消任务执行。
10. Two-Phase Termination
这一节介绍如何停止线程,我们刚开始学习线程时,可能很容易犯的错就是调用Thread
的stop
方法停止线程,该方法确实能迅速停止线程,并会让线程抛出异常。但是调用stop
方法是不安全的,如果该线程正获取了某个对象的锁,那么这个锁是不会被释放的,其他线程将继续被阻塞在该锁的条件队列里,并且也许线程正在做的工作是不能被打断的,这样可能会造成系统破坏。从线程角度看,只有执行任务的线程本身知道该何时恰当的停止执行任务,故此我们需要用Two-Phase Termination
模式来停止线程,在该模式里如果想停止某个线程,先设置请求线程停止的标志为true
,然后调用Thread
的interrupt
方法,在该线程里每完成一定工作会检查请求线程停止的标志,如果为true
,则安全地结束线程。
示例程序:
public class CountupThread extends Thread {
private long counter = 0;
private volatile boolean _shutdown_requested = false;
public void shutdownRequest() {
_shutdown_requested = true;
interrupt();
}
public boolean isShutdownRequested() {
return _shutdown_requested;
}
@Override
public void run() {
try {
while (!_shutdown_requested) {
doWork();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
doShutdown();
}
}
private void doWork() throws InterruptedException {
counter++;
System.out.println("doWork: counter = " + counter);
Thread.sleep(500);
}
private void doShutdown() {
System.out.println("doShutDown: counter = " + counter);
}
}
外界可调用shutdownRequest
来停止线程。
适用场景:
需要停止线程时,可考虑使用Two-Phase Termination
模式
注意事项:
我们在请求线程停止时,若只设置请求停止标志,是不够的,因为如果线程正在执行sleep
操作,那么会等sleep
操作执行完后,再执行到检查停止标志的语句才会退出,这样程序响应性不好。
响应停止请求的线程如果只检查中断状态(不是说我们设置的停止标志)也是不够的,如果线程正在sleep
或者wait
,则会抛出InterruptedException
异常,就算没有抛出异常,线程也会变成中断状态,似乎我们没必要设置停止标志,只需检查InterruptedException
或者用isInterrupted
方法检查当前线程的中断状态就可以了,但是这样做会引入潜在的危险,如果该线程调用的方法忽略了InterruptedException
,或者该线程使用的对象的某个方法忽略了InterruptedException
,而这样的情况是很常见的,尤其是如果我们使用某些类库代码时,又不知其实现,即使忽略了InterruptedException
,我们也不知道,在这种情况下,我们无法检查到是否有其它线程正在请求本线程退出,故此说设置终端标志是有必要的,除非能保证线程所引用的所有对象(包括间接引用的)不会忽略InterruptedException
,或者能保存中断状态。
中断状态和InterruptedException
可以互转:
-
中断状态 -> InterruptedException
if (Thread.interrupted) { throw new InterruptedException() }
-
InterruptedException -> 中断状态
try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
-
InterruptedException -> InterruptedException
InterruptedException savedInterruptException = null; ... try { Thread.sleep(1000); } catch (InterruptedException e) { savedInterruptException=e; } ... if(savedInterruptException != null ) throw savedInterruptException;
11. Thread-Specific Storage
我们知道,如果一个对象不会被多个线程访问,那么就不存在线程安全问题。Thread-Specific Storage
模式就是这样一种设计模式,为每个线程生成单独的对象,解决线程安全问题。不过为线程生成单独的对象这些细节对于使用者来说是隐藏的,使用者只需简单使用即可。需要用到ThreadLocal
类,它是线程保管箱,为每个线程保存单独的对象。
示例程序:
Log
类
public class Log {
private static ThreadLocal<TSLog> _ts_log_collection =
new ThreadLocal<TSLog>();
public static void println(String s) {
getTSLog().println(s);
}
public static void close() {
getTSLog().close();
}
private static TSLog getTSLog() {
TSLog tsLog = _ts_log_collection.get();
if (tsLog == null) {
tsLog = new TSLog(
Thread.currentThread().getName() + "-log.txt"
);
_ts_log_collection.set(tsLog);
}
return tsLog;
}
}
TSLog
类
public class TSLog {
private PrintWriter _writer = null;
public TSLog(String fileName) {
try {
_writer = new PrintWriter(fileName);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public void println(String s) {
_writer.write(s);
}
public void close() {
_writer.close();
}
}
适用场景:
使用Thread-Specific Storgae
模式可很好的解决多线程安全问题,每个线程都有单独的对象,如果从ThreadLocal
类里获取线程独有对象的时间远小于调用对象方法的执行时间,可提高程序性能。因此在日志系统里如果可以为每个线程建立日志文件,那么特别适合使用Thread-Specific Storage
模式。
注意事项:
采用Thread-Specific Storage
模式意味着将线程特有信息放在线程外部,在示例程序里,我们将线程特有的TSLog
放在了ThreadLocal
的实例里。通常我们一般将线程特有的信息放在线程内部,比如建立一个Thread
类的子类MyThread
,我们声明的MyThread
的字段,就是线程特有的信息。因为把线程特有信息放在线程外部,每个线程访问线程独有信息时,会取出自己独有信息,但是调试时会困难一些,因为有隐藏的context
(当前线程环境), 程序以前的行为,也可能会使context
出现异常,而是造成现在的bug的真正原因,我们比较难找到线程先前的什么行为导致context
出现异常。
设计多线程程序,主体是指主动操作的对象,一般指线程,客体指线程调用的对象,一般指的是任务对象,会因为重点放在“主体”与“客体”的不同,有两种开发方式:
- Actor-based 注重主体
- Task-based 注重客体
Actor-based 注重主体,偏重于线程,由线程维护状态,将工作相关的信息都放到线程类的字段,类似这样
class Actor extends Thread {
操作者内部的状态
public void run(){
从外部取得任务,改变自己内部状态的循环
}
}
Task-based注重客体,将状态封装到任务对象里,在线程之间传递这些任务对象,这些任务对象被称为消息,请求或者命令。使用这种开发方式的最典型的例子是Worker Thread
模式,生产者消费者模式。任务类似这样:
class Task implements Runnable{
执行任务所需的信息
public void run(){
执行任务所需的处理内容
}
}
实际上这两个开发方式是混用的,本人刚设计多线程程序时,总是基于Actor-based的思维方式,甚至在解决生产者消费者问题时也使用Actor-based思维方式,造成程序结构混乱,因此最好按实际场景来,适合使用Actor-based开发方式的就使用Actor-based,适合Task-based开发方式的就使用Task-based。
12. Active Object
Active Object
模式,也称为Actor
模式。Active Object
即主动对象,它不仅拥有独立线程,并且可以从外部接收异步消息,并能配合需要返回处理结果。这里的Active Object
不是指一个对象,而是指将一群对象组织起来,对外表现为一个整体,这个整体拥有独立线程,并能接收外部的异步消息,这个整体(Active Object)
处理完异步消息后还可以返回结果给调用者。
Future Pattern
也能接收异步消息并返回处理结果,但是该模式聚焦在Future
上,不是很关注线程主动执行方面,而Activie Object
将独立线程,接收异步消息并返回处理结果这些方面看作一个整体。Active Object
模式综合利用了先前介绍的Producer-Consumer
模式,Thread-Per-Message
模式,Future
模式等多线程设计模式。
示例程序:
代码可上github下载: https://github.com/cloudchou/Multithread_ActiveObject
类图如下图所示(请点击看大图):
这是一个非常复杂的模式,ActiveObject
接口里的每个方法对应MethodRequest
的一个子类,每个方法的参数对应着MethodRequest
的一个字段,因为ActiveObject
的某些方法有返回值,故此设计了Result
抽象类,表示返回值,为了让调用和执行分离,这里使用了Future
模式,故此设计了三个类,Result
,FutureResult
,RealResult
。
也是为了分离调用和执行,还使用了生产者消费者模式,将调用转化为请求对象放到ActivationQueue
里,由SchedulerThread
实例从ActivationQueue
里不断取出请求对象,并执行。
适用场景:
这个设计模式非常复杂,是否合适要考虑问题的规模,只有大规模的问题才适合使用该模式。
注意事项:
因为这个设计模式非常复杂,故此我们在使用时,一定注意各个对象的方法由哪些线程调用。比如Proxy
对象的方法可能被多个线程同时调用,而Servant
对象被封闭在Scheduler
线程里,只有SchedulerThread
线程才会调用它的方法,故此它是线程安全的,而RealResult
可能会被多个线程使用,但它是Immutable
的,FutureResult
可能被多个线程同时调用,它封装了两个字段,故此需要使用synchronized
保护,并且是采用Guarded Suspension
模式保护FutureResult
。