读写锁的java实现

ReentrantReadWriteLock

如何保证同步

Java中的可重入读写锁ReentrantReadWriteLock是基于AQS(AbstractQueuedSynchronizer)实现的,查看源码能够发现内部有一个Sync对象继承自AbstractQueuedSynchronizer,它用来管理同步机制,java并发包下的类基本都是用它来提供同步机制的。java

再查看AQS的源码会发现其内部全是native方法及包装这些方法的一些其余方法。这些native方法都是调用本地方法,利用了运行机器CPU的CAS特性。CAS(CompareAndSet)是一种非阻塞算法来保证同步,它的效率一般要比加锁算法高不少,由于它无阻塞,无挂起和恢复,无死锁。简单来讲,比较和替换是使用一个指望值和一个变量的当前值进行比较,若是当前变量的值与咱们指望的值相等,就使用一个新值替换当前变量的值,返回true,不然返回false,线程能够选择继续作其余事情。关于CAS能够参考其余博文关于这方面的解释。算法

如何维护状态

ReentrantReadWriteLock内部维护的读写状态是由32位码表示,高16位为读状态,表示持有读锁的线程数(sharedCount),低16位为写状态,表示写锁的重入次数 (exclusiveCount),状态的改变经过AQS实现,保证同步。并发

关于ReentrantReadWriteLock的最核心部分大概就是上述两点,这里再也不细致分析具体代码实现,它注重了效率但实现方式不容易咱们理解一个读写锁到底该有什么东西。所以这里重点经过一个wait/notify版本的读写锁如何实现来深刻了解读写锁的原理。this

读写锁实现原理

一个简单的读写锁实现

先让咱们对读写访问资源的条件作个概述:线程

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

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

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

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

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

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() throws InterruptedException {
        writers--;
        notifyAll();
    }
}

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

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

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

须要注意的是,在两个释放锁的方法(unlockRead,unlockWrite)中,都调用了notifyAll方法,而不是notify。要解释这个缘由,咱们能够想象下面一种情形:

若是有线程在等待获取读锁,同时又有线程在等待获取写锁。若是这时其中一个等待读锁的线程被notify方法唤醒,但由于此时仍有请求写锁的线程存在(writeRequests>0),因此被唤醒的线程会再次进入阻塞状态。然而,等待写锁的线程一个也没被唤醒,就像什么也没发生过同样。若是用的是notifyAll方法,全部的线程都会被唤醒,而后判断可否得到其请求的锁。

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

读写锁的重入

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

  1. Thread 1 得到了读锁
  2. Thread 2 请求写锁,但由于Thread 1 持有了读锁,因此写锁请求被阻塞。
  3. Thread 1 再想请求一次读锁,但由于Thread 2处于请求写锁的状态,因此想再次获取读锁也会被阻塞。

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

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

读锁重入

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

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

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

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

    private int writers = 0;
    private int writeRequests = 0;

    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.remove(callingThread);
        } else {
            readingThreads.put(callingThread, (accessCount - 1));
        }
        notifyAll();
    }

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

    private int getReadAccessCount(Thread callingThread) {
        Integer accessCount = readingThreads.get(callingThread);
        if (accessCount == null)
            return 0;
        return accessCount;
    }

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

代码中咱们能够看到,只有在没有线程拥有写锁的状况下才容许读锁的重入。此外,重入的读锁比写锁优先级高。

写锁重入

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

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

    private int writeAccesses = 0;
    private int writeRequests = 0;
    private Thread writingThread = null;

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

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

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

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

    private boolean isWriter(Thread callingThread) {
        return writingThread == callingThread;
    }
}

注意在肯定当前线程是否可以获取写锁的时候,是如何处理的。

读锁升级到写锁

有时,咱们但愿一个拥有读锁的线程,也能得到写锁。想要容许这样的操做,要求这个线程是惟一一个拥有读锁的线程。writeLock()须要作点改动来达到这个目的:

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

    private int writeAccesses = 0;
    private int writeRequests = 0;
    private Thread writingThread = null;

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

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

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

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

    private boolean isWriter(Thread callingThread) {
        return writingThread == callingThread;
    }

    private boolean isOnlyReader(Thread callingThread) {
        return readers == 1 && readingThreads.get(callingThread) != null;
    }
}

如今ReadWriteLock类就能够从读锁升级到写锁了。

写锁降级到读锁

有时拥有写锁的线程也但愿获得读锁。若是一个线程拥有了写锁,那么天然其它线程是不可能拥有读锁或写锁了。因此对于一个拥有写锁的线程,再得到读锁,是不会有什么危险的。咱们仅仅须要对上面canGrantReadAccess方法进行简单地修改:

public class ReadWriteLock {
    private boolean canGrantReadAccess(Thread callingThread) {
        if (isWriter(callingThread))
            return true;
        if (writingThread != null)
            return false;
        if (isReader(callingThread))
            return true;
        if (writeRequests > 0)
            return false;
        return true;
    }
}

可重入的ReadWriteLock的完整实现

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

public class ReadWriteLock {

    private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();

    private int writeAccesses = 0;
    private int writeRequests = 0;
    private Thread writingThread = null;

    public synchronized void lockRead() throws InterruptedException {
        Thread callingThread = Thread.currentThread();
        while (!canGrantReadAccess(callingThread)) {
            wait();
        }

        readingThreads.put(callingThread, (getReadAccessCount(callingThread) + 1));
    }

    private boolean canGrantReadAccess(Thread callingThread) {
        if (isWriter(callingThread))
            return true;
        if (hasWriter())
            return false;
        if (isReader(callingThread))
            return true;
        if (hasWriteRequests())
            return false;
        return true;
    }

    public synchronized void unlockRead() {
        Thread callingThread = Thread.currentThread();
        if (!isReader(callingThread)) {
            throw new IllegalMonitorStateException("Calling Thread does not hold a read lock on this ReadWriteLock");
        }
        int accessCount = getReadAccessCount(callingThread);
        if (accessCount == 1) {
            readingThreads.remove(callingThread);
        } else {
            readingThreads.put(callingThread, (accessCount - 1));
        }
        notifyAll();
    }

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

    public synchronized void unlockWrite() throws InterruptedException {
        if (!isWriter(Thread.currentThread())) {
            throw new IllegalMonitorStateException("Calling Thread does not hold the write lock on this ReadWriteLock");
        }
        writeAccesses--;
        if (writeAccesses == 0) {
            writingThread = null;
        }
        notifyAll();
    }

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

    private int getReadAccessCount(Thread callingThread) {
        Integer accessCount = readingThreads.get(callingThread);
        if (accessCount == null)
            return 0;
        return accessCount;
    }

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

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

    private boolean isOnlyReader(Thread callingThread) {
        return readingThreads.size() == 1 && readingThreads.get(callingThread) != null;
    }

    private boolean hasWriter() {
        return writingThread != null;
    }

    private boolean isWriter(Thread callingThread) {
        return writingThread == callingThread;
    }

    private boolean hasWriteRequests() {
        return writeRequests > 0;
    }
}

在finally中调用unlock()

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

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

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

我的博客:www.hellolvs.com

相关文章
相关标签/搜索