Java中的读/写锁

原文链接 作者:Jakob Jenkov 译者:微凉 校对:丁一

相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。

Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。

以下是本文的主题

读/写锁的Java实现(Read / Write Lock Java Implementation)

读/写锁的重入(Read / Write Lock Reentrance)

读锁重入(Read Reentrance)

写锁重入(Write Reentrance)

读锁升级到写锁(Read to Write Reentrance)

写锁降级到读锁(Write to Read Reentrance)

可重入的ReadWriteLock的完整实现(Fully Reentrant ReadWriteLock)

在finally中调用unlock() (Calling unlock() from a finally-clause)

读/写锁的Java实现

先让我们对读写访问资源的条件做个概述:

读取 没有线程正在做写操作,且没有线程在请求写操作。

写入 没有线程正在做读写操作。

如果某个线程想要读取资源,只要没有线程正在对该资源进行写操作且没有线程请求对该资源的写操作即可。我们假设对写操作的请求比对读操作的请求更重要,就要提升写请求的优先级。此外,如果读操作发生的比较频繁,我们又没有提升写操作的优先级,那么就会产生“饥饿”现象。请求写操作的线程会一直阻塞,直到所有的读线程都从ReadWriteLock上解锁了。如果一直保证新线程的读操作权限,那么等待写操作的线程就会一直阻塞下去,结果就是发生“饥饿”。因此,只有当没有线程正在锁住ReadWriteLock进行写操作,且没有线程请求该锁准备执行写操作时,才能保证读操作继续。

当其它线程没有对共享资源进行读操作或者写操作时,某个线程就有可能获得该共享资源的写锁,进而对共享资源进行写操作。有多少线程请求了写锁以及以何种顺序请求写锁并不重要,除非你想保证写锁请求的公平性。

按照上面的叙述,简单的实现出一个读/写锁,代码如下

01public class ReadWriteLock{

02    private int readers = 0;

03    private int writers = 0;

04    private int writeRequests = 0;

05 

06    public synchronized void lockRead() 

07        throws InterruptedException{

08        while(writers > 0 || writeRequests > 0){

09            wait();

10        }

11        readers++;

12    }

13 

14    public synchronized void unlockRead(){

15        readers--;

16        notifyAll();

17    }

18 

19    public synchronized void lockWrite() 

20        throws InterruptedException{

21        writeRequests++;

22 

23        while(readers > 0 || writers > 0){

24            wait();

25        }

26        writeRequests--;

27        writers++;

28    }

29 

30    public synchronized void unlockWrite() 

31        throws InterruptedException{

32        writers--;

33        notifyAll();

34    }

35}

ReadWriteLock类中,读锁和写锁各有一个获取锁和释放锁的方法。

读锁的实现在lockRead()中,只要没有线程拥有写锁(writers==0),且没有线程在请求写锁(writeRequests ==0),所有想获得读锁的线程都能成功获取。

写锁的实现在lockWrite()中,当一个线程想获得写锁的时候,首先会把写锁请求数加1(writeRequests++),然后再去判断是否能够真能获得写锁,当没有线程持有读锁(readers==0 ),且没有线程持有写锁(writers==0)时就能获得写锁。有多少线程在请求写锁并无关系。

需要注意的是,在两个释放锁的方法(unlockRead,unlockWrite)中,都调用了notifyAll方法,而不是notify。要解释这个原因,我们可以想象下面一种情形:

如果有线程在等待获取读锁,同时又有线程在等待获取写锁。如果这时其中一个等待读锁的线程被notify方法唤醒,但因为此时仍有请求写锁的线程存在(writeRequests>0),所以被唤醒的线程会再次进入阻塞状态。然而,等待写锁的线程一个也没被唤醒,就像什么也没发生过一样(译者注:信号丢失现象)。如果用的是notifyAll方法,所有的线程都会被唤醒,然后判断能否获得其请求的锁。

用notifyAll还有一个好处。如果有多个读线程在等待读锁且没有线程在等待写锁时,调用unlockWrite()后,所有等待读锁的线程都能立马成功获取读锁 —— 而不是一次只允许一个。

读/写锁的重入

上面实现的读/写锁(ReadWriteLock) 是不可重入的,当一个已经持有写锁的线程再次请求写锁时,就会被阻塞。原因是已经有一个写线程了——就是它自己。此外,考虑下面的例子:

Thread 1 获得了读锁

Thread 2 请求写锁,但因为Thread 1 持有了读锁,所以写锁请求被阻塞。

Thread 1 再想请求一次读锁,但因为Thread 2处于请求写锁的状态,所以想再次获取读锁也会被阻塞。

上面这种情形使用前面的ReadWriteLock就会被锁定——一种类似于死锁的情形。不会再有线程能够成功获取读锁或写锁了。

为了让ReadWriteLock可重入,需要对它做一些改进。下面会分别处理读锁的重入和写锁的重入。

读锁重入

为了让ReadWriteLock的读锁可重入,我们要先为读锁重入建立规则:

要保证某个线程中的读锁可重入,要么满足获取读锁的条件(没有写或写请求),要么已经持有读锁(不管是否有写请求)。

要确定一个线程是否已经持有读锁,可以用一个map来存储已经持有读锁的线程以及对应线程获取读锁的次数,当需要判断某个线程能否获得读锁时,就利用map中存储的数据进行判断。下面是方法lockRead和unlockRead修改后的的代码:

01public class ReadWriteLock{

02    private Map readingThreads =

03        new HashMap();

04 

05    private int writers = 0;

06    private int writeRequests = 0;

07 

08    public synchronized void lockRead() 

09        throws InterruptedException{

10        Thread callingThread = Thread.currentThread();

11        while(! canGrantReadAccess(callingThread)){

12            wait(); 

13        }

14 

15        readingThreads.put(callingThread,

16            (getAccessCount(callingThread) + 1));

17    }

18 

19    public synchronized void unlockRead(){

20        Thread callingThread = Thread.currentThread();

21        int accessCount = getAccessCount(callingThread);

22        if(accessCount == 1) { 

23            readingThreads.remove(callingThread); 

24        } else {

25            readingThreads.put(callingThread, (accessCount -1)); 

26        }

27        notifyAll();

28    }

29 

30    private boolean canGrantReadAccess(Thread callingThread){

31        if(writers > 0) return false;

32        if(isReader(callingThread) return true;

33        if(writeRequests > 0) return false;

34        return true;

35    }

36 

37    private int getReadAccessCount(Thread callingThread){

38        Integer accessCount = readingThreads.get(callingThread);

39        if(accessCount == null) return 0;

40        return accessCount.intValue();

41    }

42 

43    private boolean isReader(Thread callingThread){

44        return readingThreads.get(callingThread) != null;

45    }

46}

代码中我们可以看到,只有在没有线程拥有写锁的情况下才允许读锁的重入。此外,重入的读锁比写锁优先级高。

写锁重入

仅当一个线程已经持有写锁,才允许写锁重入(再次获得写锁)。下面是方法lockWrite和unlockWrite修改后的的代码。

01public class ReadWriteLock{

02    private Map readingThreads =

03        new HashMap();

04 

05    private int writeAccesses    = 0;

06    private int writeRequests    = 0;

07    private Thread writingThread = null;

08 

09    public synchronized void lockWrite() 

10        throws InterruptedException{

11        writeRequests++;

12        Thread callingThread = Thread.currentThread();

13        while(!canGrantWriteAccess(callingThread)){

14            wait();

15        }

16        writeRequests--;

17        writeAccesses++;

18        writingThread = callingThread;

19    }

20 

21    public synchronized void unlockWrite() 

22        throws InterruptedException{

23        writeAccesses--;

24        if(writeAccesses == 0){

25            writingThread = null;

26        }

27        notifyAll();

28    }

29 

30    private boolean canGrantWriteAccess(Thread callingThread){

31        if(hasReaders()) return false;

32        if(writingThread == null) return true;

33        if(!isWriter(callingThread)) return false;

34        return true;

35    }

36 

37    private boolean hasReaders(){

38        return readingThreads.size() > 0;

39    }

40 

41    private boolean isWriter(Thread callingThread){

42        return writingThread == callingThread;

43    }

44}

注意在确定当前线程是否能够获取写锁的时候,是如何处理的。

读锁升级到写锁

有时,我们希望一个拥有读锁的线程,也能获得写锁。想要允许这样的操作,要求这个线程是唯一一个拥有读锁的线程。writeLock()需要做点改动来达到这个目的:

01public class ReadWriteLock{

02    private Map readingThreads =

03        new HashMap();

04 

05    private int writeAccesses    = 0;

06    private int writeRequests    = 0;

07    private Thread writingThread = null;

08 

09    public synchronized void lockWrite() 

10        throws InterruptedException{

11        writeRequests++;

12        Thread callingThread = Thread.currentThread();

13        while(!canGrantWriteAccess(callingThread)){

14            wait();

15        }

16        writeRequests--;

17        writeAccesses++;

18        writingThread = callingThread;

19    }

20 

21    public synchronized void unlockWrite() throws InterruptedException{

22        writeAccesses--;

23        if(writeAccesses == 0){

24            writingThread = null;

25        }

26        notifyAll();

27    }

28 

29    private boolean canGrantWriteAccess(Thread callingThread){

30        if(isOnlyReader(callingThread)) return true;

31        if(hasReaders()) return false;

32        if(writingThread == null) return true;

33        if(!isWriter(callingThread)) return false;

34        return true;

35    }

36 

37    private boolean hasReaders(){

38        return readingThreads.size() > 0;

39    }

40 

41    private boolean isWriter(Thread callingThread){

42        return writingThread == callingThread;

43    }

44 

45    private boolean isOnlyReader(Thread thread){

46        return readers == 1 && readingThreads.get(callingThread) != null;

47    }

48}

现在ReadWriteLock类就可以从读锁升级到写锁了。

写锁降级到读锁

有时拥有写锁的线程也希望得到读锁。如果一个线程拥有了写锁,那么自然其它线程是不可能拥有读锁或写锁了。所以对于一个拥有写锁的线程,再获得读锁,是不会有什么危险的。我们仅仅需要对上面canGrantReadAccess方法进行简单地修改:

1public class ReadWriteLock{

2    private boolean canGrantReadAccess(Thread callingThread){

3        if(isWriter(callingThread)) return true;

4        if(writingThread != null) return false;

5        if(isReader(callingThread) return true;

6        if(writeRequests > 0) return false;

7        return true;

8    }

9}

可重入的ReadWriteLock的完整实现

下面是完整的ReadWriteLock实现。为了便于代码的阅读与理解,简单对上面的代码做了重构。重构后的代码如下。

001public class ReadWriteLock{

002    private Map readingThreads =

003        new HashMap();

004 

005    private int writeAccesses    = 0;

006    private int writeRequests    = 0;

007    private Thread writingThread = null;

008 

009    public synchronized void lockRead() 

010        throws InterruptedException{

011        Thread callingThread = Thread.currentThread();

012        while(! canGrantReadAccess(callingThread)){

013            wait();

014        }

015 

016        readingThreads.put(callingThread,

017            (getReadAccessCount(callingThread) + 1));

018    }

019 

020    private boolean canGrantReadAccess(Thread callingThread){

021        if(isWriter(callingThread)) return true;

022        if(hasWriter()) return false;

023        if(isReader(callingThread)) return true;

024        if(hasWriteRequests()) return false;

025        return true;

026    }

027 

028 

029    public synchronized void unlockRead(){

030        Thread callingThread = Thread.currentThread();

031        if(!isReader(callingThread)){

032            throw new IllegalMonitorStateException(

033                "Calling Thread does not" +

034                " hold a read lock on this ReadWriteLock");

035        }

036        int accessCount = getReadAccessCount(callingThread);

037        if(accessCount == 1){ 

038            readingThreads.remove(callingThread); 

039        } else { 

040            readingThreads.put(callingThread, (accessCount -1));

041        }

042        notifyAll();

043    }

044 

045    public synchronized void lockWrite() 

046        throws InterruptedException{

047        writeRequests++;

048        Thread callingThread = Thread.currentThread();

049        while(!canGrantWriteAccess(callingThread)){

050            wait();

051        }

052        writeRequests--;

053        writeAccesses++;

054        writingThread = callingThread;

055    }

056 

057    public synchronized void unlockWrite() 

058        throws InterruptedException{

059        if(!isWriter(Thread.currentThread()){

060        throw new IllegalMonitorStateException(

061            "Calling Thread does not" +

062            " hold the write lock on this ReadWriteLock");

063        }

064        writeAccesses--;

065        if(writeAccesses == 0){

066            writingThread = null;

067        }

068        notifyAll();

069    }

070 

071    private boolean canGrantWriteAccess(Thread callingThread){

072        if(isOnlyReader(callingThread)) return true;

073        if(hasReaders()) return false;

074        if(writingThread == null) return true;

075        if(!isWriter(callingThread)) return false;

076        return true;

077    }

078 

079 

080    private int getReadAccessCount(Thread callingThread){

081        Integer accessCount = readingThreads.get(callingThread);

082        if(accessCount == null) return 0;

083        return accessCount.intValue();

084    }

085 

086 

087    private boolean hasReaders(){

088        return readingThreads.size() > 0;

089    }

090 

091    private boolean isReader(Thread callingThread){

092        return readingThreads.get(callingThread) != null;

093    }

094 

095    private boolean isOnlyReader(Thread callingThread){

096        return readingThreads.size() == 1 &&

097            readingThreads.get(callingThread) != null;

098    }

099 

100    private boolean hasWriter(){

101        return writingThread != null;

102    }

103 

104    private boolean isWriter(Thread callingThread){

105        return writingThread == callingThread;

106    }

107 

108    private boolean hasWriteRequests(){

109        return this.writeRequests > 0;

110    }

111}

在finally中调用unlock()

在利用ReadWriteLock来保护临界区时,如果临界区可能抛出异常,在finally块中调用readUnlock()和writeUnlock()就显得很重要了。这样做是为了保证ReadWriteLock能被成功解锁,然后其它线程可以请求到该锁。这里有个例子:

1lock.lockWrite();

2try{

3    //do critical section code, which may throw exception

4} finally {

5    lock.unlockWrite();

6}

上面这样的代码结构能够保证临界区中抛出异常时ReadWriteLock也会被释放。如果unlockWrite方法不是在finally块中调用的,当临界区抛出了异常时,ReadWriteLock 会一直保持在写锁定状态,就会导致所有调用lockRead()或lockWrite()的线程一直阻塞。唯一能够重新解锁ReadWriteLock的因素可能就是ReadWriteLock是可重入的,当抛出异常时,这个线程后续还可以成功获取这把锁,然后执行临界区以及再次调用unlockWrite(),这就会再次释放ReadWriteLock。但是如果该线程后续不再获取这把锁了呢?所以,在finally中调用unlockWrite对写出健壮代码是很重要的。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: Java中的读/写锁

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 1.解决信号量丢失和假唤醒 public class MyWaitNotify3{ MonitorObject m...
    Q罗阅读 873评论 0 1
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 在一般性开发中,笔者经常看到很多同学在对待java并发开发模型中只会使用一些基础的方法。比如Volatile,sy...
    张勇_bf29阅读 699评论 0 1
  • 摘要: 我们已经知道,synchronized 是Java的关键字,是Java的内置特性,在JVM层面实现了对临界...
    kingZXY2009阅读 1,829评论 0 20
  • 你都已经嫁人, 可再一次, 我想起, 我曾为你写过情书。 就像是一盏灯点了好久, 我也说不清为什么, 明明它没有灯...
    简村小吹阅读 159评论 0 2