Basic Of Concurrency(十五: Java中的读写锁)

读写锁是一个比前文Java中的锁更加复杂的锁.想象一下当你有一个应用须要对资源进行读写,然而对资源的读取次数远大于写入.当有两个线程对同一个资源进行读取时并不会有并发问题,因此多个线程能够在同一个时间点安全的读取资源.可是当一个线程须要对资源进行写入时,则其余线程的读写都不能同时进行.容许多个读线程同时操做但只容许一个写线程写入资源,这种状况咱们能够经过读写锁来解决.html

Java5中的java.util.concurrent包中已有读写锁的实现.但咱们仍是颇有必要知道它的底层远离.java

Java中读写锁的实现

首先让咱们理清读写资源时所须要的条件:安全

读操做 若是没有线程正在进行写操做而且没有线程请求进行写操做.并发

写操做 若是没有线程正在进行写操做和读操做post

只有没有线程正在写入资源或是请求写入资源时,线程就能够进行读取操做.若是咱们确认写操做比读操做重要的多,咱们须要升级写操做的优先级,若是咱们没有这么作,那么在读操做太多频繁时候,可能会出现饥饿现象.线程请求写入操做会一直阻塞到全部的读取线程释放读写锁为止.若是新进行的读线程一直抢占先机,那么写线程可能会无限期的等待下去.结果就是产生饥饿现象.因此一个线程只能在没有线程进行写操做或是请求进行写操做时才能进行读取操做.spa

写线程只有在没有线程进行读写操做是才能进行.除非你想确保线程写入请求的公平性,否则你能够忽略有多少线程进行写操做请求和它们的顺序.线程

基于以上给出的限制条件,咱们能够实现一个公平锁,以下所示:code

public class ReadWriteLock {
    private int readers = 0;
    private int writers = 0;
    private int writeRequests = 0;
    
    public synchronized void lockRead() throws InterruptedException {
        while(writers > 0 || writeRequests > 0){
            wait();
        }
        readers++;
    }
    
    public synchronized void unlockRead(){
        readers--;
        notifyAll();
    }
    
    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        while (readers > 0 || writers> 0){
            wait();
        }
        writeRequests--;
        writers++;
    }
    
    public synchronized void unlockWrite(){
        writers--;
        notifyAll();
    }
}
复制代码

ReadWrite对象中,一共有两个lock()方法和两个unlock方法,一对读操做的lock()和unlock()方法以及一对写操做的lock()和unlock()方法.htm

对于读操做的限制在lockRead()方法中实现.读操做只有在一个写线程取得读写锁或是有一到多个写请求的状况下才会阻塞等待.对象

对于写操做的限制在lockWrite()方法中实现.一个线程想要进行写操做首先要进行写请求(writeRequests++).而后才去检查是否已经有读或者写线程取得读写锁.若没有则取得读写所进行写入操做,若有则进入while循环内部调用wait()方法进入等待状态.这个时候当前有多少个写入请求对本次操做没有任何影响.

咱们能够注意到,跟以往不一样的是,咱们在unlockRead()和unlockWrite()方法中用notifyAll()来代替notify().咱们能够想象一下下面这种状况:

在读写锁中同时有读线程和写线程在等待中.若是经过notify()唤醒的线程是读线程时,它会立刻从新进入等待状态.由于已经有一个写线程在等待中了,即已经存在一个写请求了.然而在没有任何写线程被唤醒状况下,将不会发生任何事情.若是换成调用notifyAll()的话,会唤醒全部等待中的线程,不管读写.而不是一个一个唤醒.

调用notifyAll()还有一个优点,即同时存在多个读线程的状况下,若是unlockWrite()被调用,全部读线程都可以被同时唤醒和同时操做,不用一个个来.

可重入的读写锁

上文中给出的ReadWrite.class示例并不支持可重入.若是一个线程屡次发起写入请求,即屡次尝试获取读写锁,将会陷入阻塞,由于此前已经有一个写线程获取到读写锁了,那就是它本身.可重入须要考虑如下几种状况:

  1. 线程1得到读权限
  2. 线程2请求进行写操做,但进入阻塞,由于当前有一个读线程正在进行中
  3. 线程1再次发起读请求(尝试再次获取读写锁),此次线程1陷入阻塞,由于当前已有一个写请求存在.

这种状况上文给出的ReadWriteLock.class将会陷入跟死锁相似的境地.不管线程读写都会陷入阻塞.

为了让ReadWriteLock.class支持可重入,须要作出一点更改.读写操做的可重入性须要分开处理.

读操做的可重入性

为了让ReadWiriteLock支持读操做的可重入性,咱们须要补充下面限制:

  • 一个读线程可以屡次进行读操做,在它已经得到读权限的状况下.

为了确认一个读线程得到多少次读写锁,咱们须要一个Map引用来记录每个读线程获取到读写锁的次数.当决定调用线程允不容许得到读写锁时须要检查Map引用中是否存在该线程.对lockRead()和unlockRead()的改写以下:

public class ReadWriteLock {
    private int writers = 0;
    private int writeRequests = 0;
    private Map<Thread, Integer> readingThreads = new HashMap<>();

    public synchronized void lockRead() throws InterruptedException {
        Thread callingThread = Thread.currentThread();
        while (!canGrantReadAccess(callingThread)) {
            wait();
        }
        readingThreads.put(callingThread, getReadAccessCount(callingThread) + 1);
    }

    public synchronized void unlockRead() {
        Thread callingThread = Thread.currentThread();
        int accessCount = getReadAccessCount(callingThread);
        if (accessCount > 1) {
            readingThreads.put(callingThread, (accessCount - 1));
        } else {
            readingThreads.remove(callingThread);
        }
        notifyAll();
    }

    private boolean canGrantReadAccess(Thread callingThread) {
        if (writers > 0) return false;
        if (isReader(callingThread)) return true;
        return writeRequests == 0;
    }

    private boolean isReader(Thread callingThread) {
        return readingThreads.get(callingThread) != null;
    }

    private int getReadAccessCount(Thread callingThread) {
        int accessCount = 0;
        if (readingThreads.containsKey(callingThread)) {
            accessCount = readingThreads.get(callingThread) + 1;
        }
        return accessCount;
    }
}
复制代码

你能够看到,在没有线程对资源进行写入操做时,读线程能够屡次获取到读取权限.更多的,当调用线程已经得到过读操做时将优先其余写入请求获取到读写锁.

写操做的可重入性

写操做只有在已经得到写权限的状况下才能屡次获取读写锁.对lockWrite()和unlockWrite()的改写以下:

public class ReadWriteLock {
    private int writerAccess = 0;
    private int writeRequests = 0;
    private Map<Thread, Integer> readingThreads = new HashMap<>();
    private Thread writingThread = null;

    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        Thread callingThread = Thread.currentThread();
        while (!canGrantWriteAccess(callingThread)) {
            wait();
        }
        writeRequests--;
        writerAccess++;
        writingThread = callingThread;
    }

    public synchronized void unlockWrite() {
        writerAccess--;
        if(writerAccess == 0) {
            writingThread = null;
        }
        notifyAll();
    }

    private boolean canGrantWriteAccess(Thread callingThread) {
        if (hasReaders()) return false;
        if (writingThread == null) return true;
        return isWriter(callingThread);
    }

    private boolean hasReaders() {
        return readingThreads.size() > 0;
    }

    private boolean isWriter(Thread callingThread) {
        return writingThread == callingThread;
    }
}
复制代码

咱们须要将当前取得读写锁的读线程引用起来,以便决定当前调用线程是否已经得到过写权限,容许屡次获取读写锁.

写操做到读操做的可重入性

有时候一个线程写操做完成后须要进行读操做.咱们容许一个已经得到写权限的线程得到读权限.同一个线程同时进行读写并不会有并发问题.咱们须要对canGrantReadAccess()进行以下改写:

private boolean canGrantReadAccess(Thread callingThread) {
        if(isWriter(callingThread)) return true;
        if (writers > 0) return false;
        if (isReader(callingThread)) return true;
        return writeRequests == 0;
    }
复制代码

一个完整的读写锁实现

下面是一个完整的读写锁实例.

public class ReadWriteLock {
    private int writerAccess = 0;
    private int writeRequests = 0;
    private Map<Thread, Integer> readingThreads = new HashMap<>();
    private Thread writingThread = null;

    public synchronized void lockRead() throws InterruptedException {
        Thread callingThread = Thread.currentThread();
        while (!canGrantReadAccess(callingThread)) {
            wait();
        }
        readingThreads.put(callingThread, getReadAccessCount(callingThread) + 1);
    }

    public synchronized void unlockRead() {
        Thread callingThread = Thread.currentThread();
        int accessCount = getReadAccessCount(callingThread);
        if (accessCount > 1) {
            readingThreads.put(callingThread, (accessCount - 1));
        } else {
            readingThreads.remove(callingThread);
        }
        notifyAll();
    }

    private boolean canGrantReadAccess(Thread callingThread) {
        if(isWriter(callingThread)) return true;
        if (writerAccess > 0) return false;
        if (isReader(callingThread)) return true;
        return writeRequests == 0;
    }

    private boolean isReader(Thread callingThread) {
        return readingThreads.get(callingThread) != null;
    }

    private int getReadAccessCount(Thread callingThread) {
        int accessCount = 0;
        if (readingThreads.containsKey(callingThread)) {
            accessCount = readingThreads.get(callingThread) + 1;
        }
        return accessCount;
    }

    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        Thread callingThread = Thread.currentThread();
        while (!canGrantWriteAccess(callingThread)) {
            wait();
        }
        writeRequests--;
        writerAccess++;
        writingThread = callingThread;
    }

    public synchronized void unlockWrite() {
        writerAccess--;
        if(writerAccess == 0) {
            writingThread = null;
            notifyAll();
        }
    }

    private boolean canGrantWriteAccess(Thread callingThread) {
        if (hasReaders()) return false;
        if (writingThread == null) return true;
        return isWriter(callingThread);
    }

    private boolean hasReaders() {
        return readingThreads.size() > 0;
    }

    private boolean isWriter(Thread callingThread) {
        return writingThread == callingThread;
    }
}
复制代码

在finally语句中调用unlock()

当使用ReadWriteLock来保证临界区代码的同步时,临界区中的代码可能会抛出异常。因此颇有必要在finally语句中来调用unlockRead()和unlockWrite()方法。不管线程执行的代码异常与否,始终可以释放它所持有的读写锁,以让其余线程能正常执行。以下所示:

lock.lockWrite();
        try {
            //do critical section code, which may throw exception
        } finally {
            lock.unlockWrite();
        }
复制代码

咱们使用这个小结构来保证即便临界区代码抛出异常,线程也能正常释放掉所持有的读写锁。若是咱们没有这么作,一旦临界区代码出现异常,线程将没法释放读写锁,以致于其余调用了该读写锁的lockRead()和lockWrite()方法的线程将会永远等待下去。

该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: Java中的锁
下一篇: Reentrance Lockout

相关文章
相关标签/搜索