多線程讀寫同一個對象的數據是很普遍的,通常,要避免讀寫衝突,必須保證任何時候僅有一個線程在寫入,有線程正在讀取的時候,寫入操作就必須等待。簡單說,就是要避免“寫-寫”衝突和“讀-寫”衝突。但是同時讀是允許的,因爲“讀-讀”不衝突,而且很安全。
要實現以上的ReadWriteLock,簡單的使用synchronized就不行,我們必須自己設計一個ReadWriteLock類,在讀之前,必須先獲得“讀鎖”,寫之前,必須先獲得“寫鎖”。舉例說明:
DataHandler對象保存了一個可讀寫的char[]數組:
package com.crackj2ee.thread;
public class DataHandler {
// store
data:
private
char[] buffer = "AAAAAAAAAA".toCharArray();
private
char[] doRead() {
char[] ret = new char[buffer.length];
for(int i=0; i<buffer.length; i++) {
ret[i] = buffer[i];
sleep(3);
}
return ret;
}
private void
doWrite(char[] data) {
if(data!=null) {
buffer = new char[data.length];
for(int i=0; i<buffer.length; i++) {
buffer[i] = data[i];
sleep(10);
}
}
}
private void
sleep(int ms) {
try {
Thread.sleep(ms);
}
catch(InterruptedException ie) {}
}
}
doRead()和doWrite()方法是非線程安全的讀寫方法。爲了演示,加入了sleep(),並設置讀的速度大約是寫的3倍,這符合通常的情況。
爲了讓多線程能安全讀寫,我們設計了一個ReadWriteLock:
package com.crackj2ee.thread;
public class ReadWriteLock {
private int
readingThreads = 0;
private int
writingThreads = 0;
private int
waitingThreads = 0; // waiting for write
private
boolean preferWrite = true;
public
synchronized void readLock() throws InterruptedException {
while(writingThreads>0 || (preferWrite
&&
waitingThreads>0))
this.wait();
readingThreads++;
}
public
synchronized void readUnlock() {
readingThreads--;
preferWrite = true;
notifyAll();
}
public
synchronized void writeLock() throws InterruptedException {
waitingThreads++;
try {
while(readingThreads>0 ||
writingThreads>0)
this.wait();
}
finally {
waitingThreads--;
}
writingThreads++;
}
public
synchronized void writeUnlock() {
writingThreads--;
preferWrite = false;
notifyAll();
}
}
readLock()用於獲得讀鎖,readUnlock()釋放讀鎖,writeLock()和writeUnlock()一樣。由於鎖用完必須釋放,因此,必須保證lock和unlock匹配。我們修改DataHandler,加入ReadWriteLock:
package com.crackj2ee.thread;
public class DataHandler {
// store
data:
private
char[] buffer = "AAAAAAAAAA".toCharArray();
//
lock:
private
ReadWriteLock lock = new ReadWriteLock();
public
char[] read(String name) throws InterruptedException {
System.out.println(name + " waiting for read...");
lock.readLock();
try {
char[] data = doRead();
System.out.println(name + " reads data: " + new
String(data));
return data;
}
finally {
lock.readUnlock();
}
}
public void
write(String name, char[] data) throws InterruptedException {
System.out.println(name + " waiting for write...");
lock.writeLock();
try {
System.out.println(name + " wrote data: " + new
String(data));
doWrite(data);
}
finally {
lock.writeUnlock();
}
}
private
char[] doRead() {
char[] ret = new char[buffer.length];
for(int i=0; i<buffer.length; i++) {
ret[i] = buffer[i];
sleep(3);
}
return ret;
}
private void
doWrite(char[] data) {
if(data!=null) {
buffer = new char[data.length];
for(int i=0; i<buffer.length; i++) {
buffer[i] = data[i];
sleep(10);
}
}
}
private void
sleep(int ms) {
try {
Thread.sleep(ms);
}
catch(InterruptedException ie) {}
}
}
public方法read()和write()完全封裝了底層的ReadWriteLock,因此,多線程可以安全地調用這兩個方法:
// ReadingThread不斷讀取數據:
package com.crackj2ee.thread;
public class ReadingThread extends Thread {
private
DataHandler handler;
public
ReadingThread(DataHandler handler) {
this.handler = handler;
}
public void
run() {
for(;;) {
try {
char[] data = handler.read(getName());
Thread.sleep((long)(Math.random()*1000+100));
}
catch(InterruptedException ie) {
break;
}
}
}
}
// WritingThread不斷寫入數據,每次寫入的都是10個相同的字符:
package com.crackj2ee.thread;
public class WritingThread extends Thread {
private
DataHandler handler;
public
WritingThread(DataHandler handler) {
this.handler = handler;
}
public void
run() {
char[] data = new char[10];
for(;;) {
try {
fill(data);
handler.write(getName(), data);
Thread.sleep((long)(Math.random()*1000+100));
}
catch(InterruptedException ie) {
break;
}
}
}
// 產生一個A-Z隨機字符,填入char[10]:
private void
fill(char[] data) {
char c = (char)(Math.random()*26+'A');
for(int i=0; i<data.length; i++)
data[i] = c;
}
}
最後Main負責啓動這些線程:
package com.crackj2ee.thread;
public class Main {
public
static void main(String[] args) {
DataHandler handler = new DataHandler();
Thread[] ts = new Thread[] {
new ReadingThread(handler),
new ReadingThread(handler),
new ReadingThread(handler),
new ReadingThread(handler),
new ReadingThread(handler),
new WritingThread(handler),
new WritingThread(handler)
};
for(int i=0; i<ts.length; i++) {
ts[i].start();
}
}
}
我們啓動了5個讀線程和2個寫線程,運行結果如下:
Thread-0 waiting for read...
Thread-1 waiting for read...
Thread-2 waiting for read...
Thread-3 waiting for read...
Thread-4 waiting for read...
Thread-5 waiting for write...
Thread-6 waiting for write...
Thread-4 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-0 reads data: AAAAAAAAAA
Thread-5 wrote data: EEEEEEEEEE
Thread-6 wrote data: MMMMMMMMMM
Thread-1 waiting for read...
Thread-4 waiting for read...
Thread-1 reads data: MMMMMMMMMM
Thread-4 reads data: MMMMMMMMMM
Thread-2 waiting for read...
Thread-2 reads data: MMMMMMMMMM
Thread-0 waiting for read...
Thread-0 reads data: MMMMMMMMMM
Thread-4 waiting for read...
Thread-4 reads data: MMMMMMMMMM
Thread-2 waiting for read...
Thread-5 waiting for write...
Thread-2 reads data: MMMMMMMMMM
Thread-5 wrote data: GGGGGGGGGG
Thread-6 waiting for write...
Thread-6 wrote data: AAAAAAAAAA
Thread-3 waiting for read...
Thread-3 reads data: AAAAAAAAAA
......
可以看到,每次讀/寫都是完整的原子操作,因爲我們每次寫入的都是10個相同字符。並且,每次讀出的都是最近一次寫入的內容。
如果去掉ReadWriteLock:
package com.crackj2ee.thread;
public class DataHandler {
// store
data:
private
char[] buffer = "AAAAAAAAAA".toCharArray();
public
char[] read(String name) throws InterruptedException {
char[] data = doRead();
System.out.println(name + " reads data: " + new
String(data));
return data;
}
public void
write(String name, char[] data) throws InterruptedException {
System.out.println(name + " wrote data: " + new
String(data));
doWrite(data);
}
private
char[] doRead() {
char[] ret = new char[10];
for(int i=0; i<10; i++) {
ret[i] = buffer[i];
sleep(3);
}
return ret;
}
private void
doWrite(char[] data) {
for(int i=0; i<10; i++) {
buffer[i] = data[i];
sleep(10);
}
}
private void
sleep(int ms) {
try {
Thread.sleep(ms);
}
catch(InterruptedException ie) {}
}
}
運行結果如下:
Thread-5 wrote data: AAAAAAAAAA
Thread-6 wrote data: MMMMMMMMMM
Thread-0 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-4 reads data: AAAAAAAAAA
Thread-2 reads data: MAAAAAAAAA
Thread-3 reads data: MAAAAAAAAA
Thread-5 wrote data: CCCCCCCCCC
Thread-1 reads data: MAAAAAAAAA
Thread-0 reads data: MAAAAAAAAA
Thread-4 reads data: MAAAAAAAAA
Thread-6 wrote data: EEEEEEEEEE
Thread-3 reads data: EEEEECCCCC
Thread-4 reads data: EEEEEEEEEC
Thread-1 reads data: EEEEEEEEEE
可以看到在Thread-6寫入EEEEEEEEEE的過程中,3個線程讀取的內容是不同的。
思考
java的synchronized提供了最底層的物理鎖,要在synchronized的基礎上,實現自己的邏輯鎖,就必須仔細設計ReadWriteLock。
Q: lock.readLock()爲什麼不放入try{
} 內?
A:
因爲readLock()會拋出InterruptedException,導致readingThreads++不執行,而readUnlock()在
finally{ }
中,導致readingThreads--執行,從而使readingThread狀態出錯。writeLock()也是類似的。
Q:
preferWrite有用嗎?
A:
如果去掉preferWrite,線程安全不受影響。但是,如果讀取線程很多,上一個線程還沒有讀取完,下一個線程又開始讀了,就導致寫入線程長時間無法
獲得writeLock;如果寫入線程等待的很多,一個接一個寫,也會導致讀取線程長時間無法獲得readLock。preferWrite的作用是讓讀
/寫交替執行,避免由於讀線程繁忙導致寫無法進行和由於寫線程繁忙導致讀無法進行。
Q:
notifyAll()換成notify()行不行?
A:
不可以。由於preferWrite的存在,如果一個線程剛讀取完畢,此時preferWrite=true,再notify(),若恰好喚醒的是一個讀線程,則while(writingThreads>0
|| (preferWrite &&
waitingThreads>0))可能爲true導致該讀線程繼續等待,而等待寫入的線程也處於wait()中,結果所有線程都處於wait
()狀態,誰也無法喚醒誰。因此,notifyAll()比notify()要來得安全。程序驗證notify()帶來的死鎖:
Thread-0 waiting for read...
Thread-1 waiting for read...
Thread-2 waiting for read...
Thread-3 waiting for read...
Thread-4 waiting for read...
Thread-5 waiting for write...
Thread-6 waiting for write...
Thread-0 reads data: AAAAAAAAAA
Thread-4 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-5 wrote data: CCCCCCCCCC
Thread-2 waiting for read...
Thread-1 waiting for read...
Thread-3 waiting for read...
Thread-0 waiting for read...
Thread-4 waiting for read...
Thread-6 wrote data: LLLLLLLLLL
Thread-5 waiting for write...
Thread-6 waiting for write...
Thread-2 reads data: LLLLLLLLLL
Thread-2 waiting for read...
(運行到此不動了)
注意到這種死鎖是由於所有線程都在等待別的線程喚醒自己,結果都無法醒過來。這和兩個線程希望獲得對方已有的鎖造成死鎖不同。因此多線程設計的難度遠遠高於單線程應用。
從JDK 5開始,java.util.concurrent包就已經包含了ReadWriteLock,使用更簡單,無需我們自行實現上述代碼。但是,理解ReadWriteLock的原理仍非常重要。