場景描述
相信很多開發(fā)者都遇到過多線程并發(fā)訪問共享資源的情況,那么為了防止在多線程訪問的時候出現(xiàn)線程安全問題,通常我們可以使用加鎖的方式來避免線程安全問題,可以使用synchronized關(guān)鍵字以及一些顯式鎖操作,當然在分布式場景中還可以使用一些分布式鎖機制來保證共享資源線程安全訪問。
對于共享資源的訪問,可以分為兩種操作,一種是對共享資源的讀操作,一種是對共享資源的寫操作。如果有多個線程在同一時刻對共享資源的操作都是讀操作的話,雖然是存在資源競爭的情況,但是并不會影響到最終的訪問結(jié)果,也不會引起數(shù)據(jù)不一致的情況發(fā)生。這樣時候,如果還是使用排他鎖的方式進行加鎖,很明顯就有點得不償失了。如下圖所示,共享資源被多個線程共同進行讀操作的時候,是不會出現(xiàn)線程安全問題的。

所以,如果在某個場景中,對于共享資源的讀操作要明顯多于寫操作的時候,這個時候,讀操作是可以不用進行加鎖的。這樣對系統(tǒng)的性能的提升也會非常明顯。
如圖所示,在多個線程對共享資源進行寫操作的時候,很明顯,一定會造成資源的數(shù)據(jù)一致性問題出現(xiàn)。所以對于寫操作來講,一定要注意進行加鎖的操作。
讀寫分離程序?qū)崿F(xiàn)
第一步、定義一個鎖接口
無論是對于讀鎖還是寫鎖,都離不開加鎖和解鎖兩個操作,所以在接口中需要定義兩個基本的操作,加鎖和解鎖,代碼如下。
public interface Lock {
/**
* 進行加鎖操作
* @throws InterruptedException
*/
void lock() throws InterruptedException;
/**
* 進行解鎖操作
*/
void unlock();
}
第二步、讀寫鎖接口實現(xiàn)
在定義好鎖接口之后,接下來就是需要去定義如何來操作鎖。既然需要對讀寫鎖進行判斷,那么首先就需要有關(guān)于鎖的判斷條件的規(guī)范,下面實現(xiàn)的ReadWriteLock其實就是讀寫鎖的使用規(guī)范。
public interface ReadWriteLock {
/**
* 創(chuàng)建讀鎖
* @return
*/
Lock readLock();
/**
* 創(chuàng)建寫鎖
* @return
*/
Lock writeLock();
/**
* 獲取正在執(zhí)行的寫操作個數(shù)
* @return
*/
int getDoingWriters();
/**
* 獲取正在等待執(zhí)行的寫操作個數(shù)
* @return
*/
int getWAItingWriters();
/**
* 獲取正在執(zhí)行的讀操作個數(shù)
* @return
*/
int getDoingReader();
static ReadWriteLock readWriteLock(){
return new ReadWriteLockImpl();
}
static ReadWriteLock readWriteLock(boolean preferWriter){
return new ReadWriteLockImpl(preferWriter);
}
}
會看到,在上面這個接口中定義了如下的一些操作
- 創(chuàng)建讀鎖:readLock()
- 創(chuàng)建寫鎖:writeLock()
- 獲取正在執(zhí)行的讀操作:getDoingReader()
- 獲取正在執(zhí)行的寫操作:getDoingWriters()
- 獲取等待執(zhí)行的寫操作:getWaitingWriters()
為什么要有這些操作呢?其實這樣寫操作就是來規(guī)定如何去使用讀寫鎖的,例如,讀操作的個數(shù)大于0的時候,這個適合就意味著寫操作的個數(shù)是等于0的。反之當寫操作的個數(shù)大于0 的時候,就意味著讀操作的個數(shù)是等于0的。因為在讀寫操作的過程中,讀操作和寫操作是沖突的過程,也就是說在寫的時候不能讀,在讀的時候不能寫。那么通過這樣的一個數(shù)量關(guān)系我們就可以實現(xiàn)什么時候加鎖什么時候解鎖了。
第三步、按照規(guī)則實現(xiàn)讀寫鎖。
既然有了上面的規(guī)則的定義,那么一定要有規(guī)則的實現(xiàn),下面這段代碼其實就是對上面的規(guī)則的實現(xiàn)。
public class ReadWriteLockImpl implements ReadWriteLock {
/**
* 定義鎖對象
*/
private final Object MUTEX = new Object();
private int doingWriters = 0;
private int waitingWriters = 0;
private int doingReaders = 0;
/**
* 偏好設置
*/
private boolean preferWriter;
public ReadWriteLockImpl() {
this(true);
}
public ReadWriteLockImpl(boolean preferWriter) {
this.preferWriter = preferWriter;
}
@Override
public Lock readLock() {
return new ReadLock(this);
}
@Override
public Lock writeLock() {
return new WriteLock(this);
}
void addDoingWriters(){
this.doingWriters++;
}
void addWaitingWriters(){
this.waitingWriters++;
}
void addDoingReader(){
this.doingReaders++;
}
void minusDoingWriters(){
this.doingWriters--;
}
void minusWaitingWriters(){
this.waitingWriters--;
}
void minusDoingReader(){
this.doingReaders--;
}
@Override
public int getDoingWriters() {
return this.doingWriters;
}
@Override
public int getWaitingWriters() {
return this.waitingWriters;
}
@Override
public int getDoingReader() {
return this.doingReaders;
}
Object getMutex(){
return this.MUTEX;
}
boolean getPreferWriter(){
return this.preferWriter;
}
void changePrefer(boolean preferWriter){
this.preferWriter = preferWriter;
}
}
上述代碼中包含了如下的一些內(nèi)容
- addDoingWriters() :正在執(zhí)行的寫操作加一;
- addWaitingWriters():等待執(zhí)行的寫操作加一;
- addDoingReader():正在執(zhí)行的讀操作加一;
- minusDoingWriters():正在執(zhí)行的寫操作減一;
- minusWaitingWriters():等待執(zhí)行的寫操作減一;
- minusDoingReader():正在執(zhí)行的讀操作減一;
- changePrefer(boolean preferWriter):修改偏好設置;
- Object getMutex():獲取鎖對象;
其他的操作可以先不做了解,這里重要的部分有兩部分內(nèi)容,第一是對于等待讀寫操作的增加和減少;第二則是對Object getMutex()操作的立即。
對于讀寫操作數(shù)量的判斷主要是用來進行加鎖和解鎖操作的判斷。那么Object getMutex()是用來干什么的?
Object getMutex()操作是用來獲取一個鎖對象,那么我們真正使用的ReadLock 和 WriterLock又是干嘛的?其實細心的讀者可能發(fā)現(xiàn)了Object getMutex()鎖操作其實是為了保證讀寫操作內(nèi)部的一個線程安全,也就是為了保證我們對于加鎖條件判斷的一個線程安全性的保證,而真正我們通過readLock()方法和writeLock()方法進行的讀寫操作實現(xiàn)才是讀寫鎖的重點。
實現(xiàn)讀鎖
根據(jù)上面的規(guī)則,讀鎖的條件是當前沒有再執(zhí)行的寫操作的時候就可以進行加鎖,當前沒有再有執(zhí)行的讀操作的時候就可以釋放鎖。根據(jù)規(guī)則實現(xiàn),代碼如下。
public class ReadLock implements Lock {
private final ReadWriteLockImpl readWriteLock;
public ReadLock(ReadWriteLockImpl readWriteLock) {
this.readWriteLock = readWriteLock;
}
/***
* 進行加鎖操作
*/
@Override
public void lock() throws InterruptedException {
synchronized (readWriteLock.getMutex()){
while (readWriteLock.getDoingWriters()>0
||(readWriteLock.getPreferWriter()
&&readWriteLock.getWaitingWriters()>0)){
readWriteLock.getMutex().wait();
}
readWriteLock.addDoingReader();
}
}
/**
* 進行解鎖操作
*/
@Override
public void unlock() {
synchronized (readWriteLock.getMutex()){
readWriteLock.minusDoingReader();
readWriteLock.changePrefer(true);
readWriteLock.getMutex().notifyAll();
}
}
}
實現(xiàn)寫鎖
寫鎖實現(xiàn)的條件根據(jù)上面的描述可以知道,加鎖的條件是當前沒有讀操作,解鎖的條件是當前沒有正在執(zhí)行的寫操作。代碼實現(xiàn)如下。
public class WriteLock implements Lock {
private final ReadWriteLockImpl readWriteLock;
public WriteLock(ReadWriteLockImpl readWriteLock) {
this.readWriteLock = readWriteLock;
}
/**
* 進行加鎖操作
* @throws InterruptedException
*/
@Override
public void lock() throws InterruptedException {
synchronized (readWriteLock.getMutex()){
try{
readWriteLock.addWaitingWriters();
while (readWriteLock.getDoingReader()>0
||readWriteLock.getDoingWriters()>0){
readWriteLock.getMutex().wait();
}
}finally {
this.readWriteLock.minusWaitingWriters();
}
readWriteLock.addDoingWriters();
}
}
/**
* 進行解鎖操作
*/
@Override
public void unlock() {
synchronized (readWriteLock.getMutex()){
readWriteLock.minusDoingWriters();
readWriteLock.changePrefer(true);
readWriteLock.getMutex().notifyAll();
}
}
}
分析
根據(jù)讀寫鎖分別的實現(xiàn)來看,似乎底層鎖定的就是在ReadWriteLockImpl規(guī)則中實現(xiàn)的final Object MUTEX = new Object()鎖定對象,但是仔細想來,這個對象其實就是為了保證讀寫鎖的安全性,真正能夠行決定讀寫鎖的其實是ReadWriteLockImpl對象中定義一些數(shù)量規(guī)則。通過這些數(shù)量規(guī)則的判斷決定是讀操作還是寫操作。
另外在JDK并發(fā)包
JAVA.util.concurrent.locks中提供了一些讀寫鎖的操作,如下圖所示。

通過圖中所提供的方法來看,基本的實現(xiàn)思路與上面我們的實現(xiàn)思路是一樣。我們采取的是偏向設置,而JDK提供的是公平性相關(guān)的內(nèi)容。而關(guān)于公平性相關(guān)的內(nèi)容,這里我們不做過多的介紹,有興趣的讀者可以自己研究一下啊。