Java Thread實現讀寫同步 (轉)
Thread實現讀寫同步
(wang hailong)
本文給出一個例子,說明如何使用java thread,處理讀寫者同步的問題。:namespace prefix = o ns = "urn:schemas--com::office" />
文中的能夠編譯執行。java thread.TestMain
本文可以和另一篇文章《Java Thread應該注意的問題》,進行比較閱讀。
1.讀寫者同步問題
多個讀者可以同時讀取同一個緩衝區,但當有寫者對緩衝區進行寫操作時,具有排他性質,其他的讀者都不能讀取這個緩衝區,其他的寫者也不能寫這個緩衝區。
2.程式碼構成和思路
這個例子中包括四個類。
(1)讀寫資源類RWRe,包含了讀寫者的計數,共享資源,還有所有的同步程式碼。
(2)讀者類RunnableReader。實現Runnable介面;進行讀操作。
(3)寫者類RunnableWriter。實現Runnable介面;進行寫操作。
(4)測試類TestMain。生成並執行多個寫執行緒和讀執行緒,顯示結果。
這個例子對共享資源進行“盒式封裝”,把共享資源包含在一個“盒”內。並把所有的同步程式碼都集中在“盒”裡面。讀者類和寫者類並不進行同步處理,只是申請資源,然後進行讀寫,讀寫完成之後,釋放資源。
這種方法的優點是共享資源“盒”部分的程式碼直觀易讀,緊湊可控,讀者類和寫者類不用關心同步問題。缺點是共享資源“盒”規定了嚴格的順序和呼叫規範,讀者類和寫者類必須嚴格遵守共享資源“盒”的呼叫規範,否則會造成執行緒死鎖,或者資源操作衝突。
不過,即使由讀者類和寫者類來實現執行緒同步,如果不注意,也會造成執行緒死鎖,或者資源操作衝突。這是執行緒的固有問題。:-)
下面給出這四個類的原始碼和說明。
3.讀寫資源類RWResource
package thread;
import java.util.List;
import java.util.ArrayList;
/**
* resource for reading and writing
*/
public class RWResource {
/**
* When readerNumber == 0, there is no one reading or writing.
* When readerNumber > 0, readerNumber means number of readers.
* When readerNumber < 0, it means that some writer is writing.
*/
private int readerNumber = 0;
/**
* the shared resource for writing or reading
*/
private List buffer = null;
public RWResource() {
buffer = new ArrayList(512);
readerNumber = 0;
}
/**
* get buffer for reading.
* should be called before reading
* @return the buffer
*/
public synchronized List getBufferForReading(){
// if some writer is writing, wait until no writer is writing
while(readerNumber < 0){
try{
this.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
// when readerNumber >= 0
readerNumber++;
return buffer;
}
/**
* should be called after reading
*/
public synchronized void finishReading(){
readerNumber--;
if(readerNumber == 0){
this.notifyAll(); // notify possible waiting writers
}
}
/**
* get buffer for writing.
* should be called before writing.
* @return the buffer
*/
public synchronized List getBufferForWriting(){
// if some writer is writing or some reader is reading, wait until no one is writing or reading
while(readerNumber != 0){
try{
this.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
// when readerNumber == 0
readerNumber--; // now readderNumber == -1.
return buffer;
}
/**
* should be called after writing
*/
public synchronized void finishWriting(){
readerNumber++; // readerNumber = -1 + 1 = 0;
// readerNumber must be 0 at this point
this.notifyAll(); // notify possible waiting writers or waiting readers
}
}
讀寫資源類RWResource提供了4個Synchronized方法,分成兩組,供讀者和寫者呼叫。
閱讀上面的程式碼,可以看到,讀寫資源類RWResource透過readerNumber計數控制對共享資源的讀寫訪問。當readerNumber等於0時,說明資源空閒,可以讀寫;當readerNumber大於0時,說明資源正在被一些讀者讀取,其他執行緒可以讀,不可以寫;當readerNumber小於0時(-1),說明資源被某個寫者佔用,正在寫入,其他執行緒不可以讀,也不可以寫。
讀者首先呼叫getBufferForReading()獲取共享資源,如果readerNumber大於等於0,表示沒有寫者佔用資源,讀者能夠獲取共享資源,此時,readerNumber加1,表示讀者的個數增加;讀取之後,必須呼叫finishReading()釋放資源,此時,readerNumber減1,表示讀者的個數減少。
寫者首先呼叫getBufferForWriting()獲取共享資源,如果readerNumber等於0,表示資源空閒,寫者能夠獲取到共享資源,此時,readerNumber減1,readerNumber的值變為-1,表示資源正在被寫入;寫者寫完資源之後,必須呼叫,必須呼叫finishWriting()釋放資源,此時,readerNumber加1,readerNumber的值變為0,回到空閒狀態。
另外,還請留意讀寫資源類RWResource程式碼裡面的wait()和notifyAll()呼叫。
讀者在readerNumber小於0的情況下等待,呼叫Wait();寫者在readerNumber大於0的情況下等待,呼叫Wait()。
在釋放資源時( finishReading()或finishWriting() ),如果readerNumber的值變為0,回到空閒狀態,呼叫notifyAll(),通知潛在的等待者——讀者或寫者。
4.讀者類RunnableReader
package thread;
import java.util.List;
import java.util.Iterator;
public class RunnableReader implements Runnable{
private RWResource resource = null;
public RunnableReader() {
}
/**
* must be called before start running
* @param theResource
*/
public void setRWResource(RWResource theResource){
resource = theResource;
}
public void run(){
while(true){
// get the reader's name
String readerName = "[" + Thread.currentThread().getName() + "] ";
// first, get buffer for reading
List buffer = resource.getBufferForReading();
// reading
for(Iterator iterator = buffer.iterator(); iterator.hasNext(); ){
System.out.println(readerName + iterator.next());
}
int articleNumber = buffer.size();
int thinkingTime = articleNumber * 1000;
for(int i = 0; i < thinkingTime; i++){
// thingking hard when reading
}
// finish reading
resource.finishReading();
// rest
try{
Thread.sleep(articleNumber * 50);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
上述程式碼中的setRWResource()方法傳入共享資源——讀寫資源類。本例採用引數傳遞的方法,在讀者和寫者之間共享讀寫資源類。
Run()方法實現Runnable介面的Run()方法。首先,獲取當前讀者(執行緒)的名稱,然後,試圖獲取讀資源——resource.getBufferForReading(),獲取資源之後,讀取buffer裡面的所有文章,邊讀邊思考(注意程式碼裡面的for(int i = 0; i < thinkingTime; i++)行,佔用時間,表示思考過程),最後,釋放資源——resource.finishReading()。讀完文章,讀者休息一段時間——Thread.sleep(articleNumber * 50)。
注意,在以上的過程中,一定要嚴格遵守這樣的規定,在resource.getBufferForReading()和resource.finishReading()之間,進行讀取操作。
5.寫者類RunnableWriter
package thread;
import java.util.List;
import java.util.Iterator;
public class RunnableWriter implements Runnable{
private RWResource resource = null;
private int articleNumber = 0;
public RunnableWriter() {
articleNumber = 0;
}
/**
* must be called before start running
* @param theResource
*/
public void setRWResource(RWResource theResource){
resource = theResource;
}
public void run(){
while(true){
// get the writer's name
String writerName = "[" + Thread.currentThread().getName() + "] ";
// first, get buffer for reading
List buffer = resource.getBufferForWriting();
int nWritten = 3; // write 4 articles one time
for(int n = 0; n< nWritten; n++){
// writing
articleNumber++;
String articleName = "article" + articleNumber;
buffer.add(articleName);
System.out.println(writerName + articleName);
int thinkingTime = 10000;
for (int i = 0; i < thinkingTime; i++) {
// thingking hard when writing
}
} // finish writing
resource.finishWriting();
// rest
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
上述程式碼中的setRWResource()方法傳入共享資源——讀寫資源類。本例採用引數傳遞的方法,在讀者和寫者之間共享讀寫資源類。
Run()方法實現Runnable介面的Run()方法。首先,獲取當前寫者(執行緒)的名稱,然後,試圖獲取寫資源——resource.getBufferForWriting(),獲取資源之後,開始向buffer裡面寫3篇文章,邊寫邊思考(注意程式碼裡面的for(int i = 0; i < thinkingTime; i++)行,佔用cpu時間,表示思考過程),最後,釋放資源——resource.finishWriting()。讀完文章,寫者休息一段時間——Thread.sleep(500)。
注意,在以上的過程中,一定要嚴格遵守這樣的規定,在resource.getBufferForWriting()和resource.finishWriting()之間,進行寫操作。
6.測試類TestMain
package thread;
public class TestMain{
public static void main(String[] args) {
// init 生成共享資源
RWResource resource = new RWResource();
// 生成讀者,設定共享資源
RunnableReader reader = new RunnableReader();
reader.setRWResource(resource);
// 生成寫者,設定共享資源。
RunnableWriter writer = new RunnableWriter();
writer.setRWResource(resource);
int writerNumber = 5; // 5個寫者
int readerNumber = 10; // 10個讀者
// start writers 生成5個寫執行緒,並給每個執行緒起個名字,writer1, writer2…
for(int i = 0; i < writerNumber; i++){
Thread thread = new Thread(writer, "writer" + (i+1));
thread.start();
}
// give writers enough time to think and write articles
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
// start readers生成10個讀執行緒,並給每個執行緒起個名字,reader1, reader2…
for(int i = 0; i < readerNumber; i++){
Thread thread = new Thread(reader, "reader" + (i+1));
thread.start();
}
}
}
以上的測試類TestMain程式碼,生成並執行多個寫執行緒和讀執行緒,產生的結果可能如下:
[writer1] article1
[writer1] article2
[writer1] article3
…
[reader2] article1
[reader3] article1
[reader4] article1
[reader5] article1
[reader6] article1…
[reader1] article1
…
[writer3] article67
[writer3] article68
[writer3] article69
…
我們可以看到,Writer寫的文章的號碼從不相同,而且,每個Writer每次寫3篇文章,寫的過程從來不會被打斷。每個讀者每次通讀所有的文章,經常有幾個讀者同時讀同一篇文章的情況。
6.兩種改進思路
這個例子採用“盒”包裝的方法,把“鎖”(readerNumber)和資源(buffer)放在同一個“盒子”(RWResource)裡,但是,“盒子”對資源的包裝是不完全的,只是簡單地把資源(buffer)返回給讀者和寫者,並沒有對資源(buffer)的訪問操作進行封裝。
其實,可以對資源(buffer)的訪問進行進一步封裝,比如,為“盒子”提供String[] readerBuffer()和writeBuffer(String)兩個方法,在這兩個方法裡面,根據鎖(readerNumber)的狀態,判斷讀寫操作是否合法,這樣,程式碼的整體性會更好。帶來的結果是,RWResource類的呼叫規範和順序更加嚴格,必須在resource.getBufferForReading()和resource.finishReading()呼叫readerBuffer()方法,必須在resource.getBufferForWriting()和resource.finishWriting()之間呼叫writeBuffer()方法。否則,這兩個方法會報錯。
這樣做會增加RWResource類的複雜度。還有一些設計上的因素需要考慮——readerBuffer和writeBuffer方法是否應該被synchronized修飾?
從上例看到,在resource.getBufferForReading()和resource.finishReading()之間,進行讀操作;在resource.getBufferForWriting()和resource.finishWriting()之間,進行寫操作。讀操作和寫操作部分的程式碼,是不用同步的。所以,在getBufferForReading()和finishReading()這樣的成對操作之間,用synchronized修飾readerBuffer和writeBuffer方法,是多此一舉。
但是,從程式碼的完整性角度來看,因為readerBuffer和writeBuffer方法需要讀 “鎖”的狀態,所以,readerBuffer和writeBuffer方法還是加上synchronized修飾符為好。
考慮到這些因素,本例採取了一種折衷的方法。從形式上看,“鎖”和“資源”是聚合在一起的,實際上,兩者的操作是分開的,並不相關。“盒子”根據“鎖”的狀態,調整資源的分配,讀者和寫者得到資源之後,享有對資源的完全訪問。
另一個方向是把“資源”和“鎖”完全分開。把getBufferForReading方法改成startReading,把getBufferForWriting方法改成startWriting,RWResource不再分配資源,只進行“鎖”操作。把RWResource該作RWLock類。
RunnableReader和RunnableWriter類各自增加一個setBuffer()方法,共享buffer資源。這樣,RunnableReader和RunnableWriter類就有了兩個分開的方法:setBuffer()設定共享資源,setRWLock()設定讀寫鎖。
對本例稍加修改,就可以實現上述的兩種思路。限於篇幅,這裡不能給出完整的修改程式碼。
7.補充
本例中對讀寫資源類RWResource強加了呼叫順序。
在resource.getBufferForReading()和resource.finishReading()之間,進行讀操作。
在resource.getBufferForWriting()和resource.finishWriting()之間,進行寫操作。
要求在一些處理之前,一定要執行某項特殊操作,處理之後一定也要執行某項特殊操作。這種人為的順序性,無疑增加了程式碼的耦合度,降低了程式碼的獨立性。很有可能會成為執行緒死鎖和資源操作衝突的根源。
這點一直讓我不安,可是沒有找到方法避免。畢竟,死鎖或者資源操作衝突,是執行緒的固有問題。
很巧的是,正在我惴惴不安的時候,我的一個朋友提供了一個資訊。Sun公司根據JCR,決定在1.5中引入關於concurrency(併發)的部分。
以下這個網址是concurrency部分的util.concurrent一個實現。非常好的資訊。對於處理多執行緒併發問題,很有幫助。
http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html
裡面提供了一個ReadWriteLock類,標準用法如下。
Standard usage of ReadWriteLock:
class X {
ReadWriteLock rw;
// ...
public void read() throws InterruptedException {
rw.readLock().acquire();
try {
// ... do the read
}
finally {
rw.readlock().release()
}
}
public void write() throws InterruptedException {
rw.writeLock().acquire();
try {
// ... do the write
}
finally {
rw.writelock().release()
}
}
}
我們可以看到,ReadWriteLock同樣要求呼叫的順序——aquire()和release()。我對自己的例子增強了一點信心。
我又檢視了WriterPreferenceReadWriteLock類,看到裡面成對的方法,startRead(),endRead();startWrite(),endWrite()。我的心情完全放鬆了下來。我的思路雖然粗糙,但大體的方向是正確的。
建議,本文的例子比較簡單,可以作為同步原理的簡介,之後,訪問專業化的程式碼http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html
Enjoy it. :-)
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-1004606/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java實現Kafka讀寫筆記JavaKafka筆記
- 讀寫者問題-java實現Java
- 關於利用go實現非同步讀寫的寫法分享Go非同步
- Java使用讀寫鎖替代同步鎖Java
- 資料庫讀寫分離,主從同步實現方法資料庫主從同步
- MySQL怎麼實現主從同步和Django實現MySQL讀寫分離MySql主從同步Django
- C++Builder下實現埠讀寫 (轉)C++UI
- Java Thread in JVM (轉)JavathreadJVM
- Java實現非同步呼叫Java非同步
- 帶貨直播系統,透過主從同步實現讀寫分離主從同步
- 利用訊號量semaphore實現兩個程式讀寫同步 Linux CLinux
- Java 併發包中的讀寫鎖及其實現分析Java
- Amoeba實現讀寫分離
- 轉享:JavaScript非同步實現JavaScript非同步
- @Java | Thread & synchronized – [ 執行緒同步鎖 基本使用]Javathreadsynchronized執行緒
- 關於java實現同步的方法Java
- ProxySQL實現MySQL讀寫分離MySql
- Amoeba 實現 MySQL 讀寫分離MySql
- CQRS如何實現讀寫分離
- 輕鬆掌握java讀寫鎖(ReentrantReadWriteLock)的實現原理Java
- ShardingSphere(七) 讀寫分離配置,實現分庫讀寫操作
- JAVA讀寫excelJavaExcel
- java 讀寫cookieJavaCookie
- (7)資料庫讀寫分離,主從同步實現方法(資料庫設定)資料庫主從同步
- 教你如何用 Java 實現非同步呼叫Java非同步
- AES加密 – iOS與Java的同步實現加密iOSJava
- 7.7 實現程式記憶體讀寫記憶體
- node的讀寫流簡易實現
- Amoeba+Mysql 實現讀寫分離MySql
- 【Mongo】Mongo讀寫分離的實現Go
- springboot實現讀寫分離Spring Boot
- mysql-proxy實現讀寫分離MySql
- 轉Java開發 Java 小寫轉大寫Java
- 使用ProxySQL實現MySQL Group Replication的故障轉移、讀寫分離(一)MySql
- Linux環境下實現NTFS分割槽完全讀寫的方法(轉)Linux
- 語音轉寫可實時,直播也能同步字幕
- Java CompletableFuture 非同步超時實現探索Java非同步
- PIO讀取excel文件,java實現ExcelJava