多線程程序的評量標準
-
安全性:不損壞對象
不安全是指,對象的狀態處於非預期狀態,比如賬戶餘額變成了負值 -
生存性:進行必要的處理
生存性是指:程序能正常運行,可進行必要的處理,影響生存性的典型問題有出現死鎖 -
複用性:可再利用類
複用性是指代碼重用,若複用性好,可減少大量重複代碼 -
性能:能快速,大量進行處理
性能有兩個方面的考慮因素:吞吐量和響應性,客戶端程序比較重視響應性,服務端程序更重視吞吐量,吞吐量是指單位時間內完成的任務,響應性是指提交任務後多長時間內能收到程序的反饋。比如說我們在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
。