Java中的ReadWriteLock是什么?
一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果,Java中的ReadWriteLock是Java5中新增的一个接口,提供了readLock和writeLock两种锁机制。
一个ReadWriteLock维护一对关联的锁,一个用于只读操作,一个用于写;在没有写线程的情况下,一个读锁可能会同时被多个读线程持有,写锁是独占的。
我们来看一下ReadWriteLock的源码:
public interface ReadWriteLock{ Lock readLock(); Lock writeLock(); }
解读:
从源码上面我们可以看出来ReadWriteLock并不是Lock的子接口,只不过ReadWriteLock借助Lock来实现读写两个锁并存、互斥的操作机制。
在ReadWriteLock中每次读取共享数据时需要读取锁,当修改共享数据时需要写入锁,这看起来好像是两个锁,但是并非如此。
ReentrantReadWriteLockReentrantReadWriteLock是ReadWriteLock在java.util里面唯一的实现类,主要使用场景是当有很多线程都从某个数据结构中读取数据,而很少有线程对其进行修改。
示例
例一
ReadLock和WriteLock单独使用的情况
package demo.thread; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockDemo { public static void main(String[] args) { final Count ct = new Count(); for (int i = 0; i < 2; i++) { new Thread() { @Override public void run() { ct.get(); } }.start(); } for (int i = 0; i < 2; i++) { new Thread() { @Override public void run() { ct.put(); } }.start(); } } } class Count { private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public void get() { rwl.readLock().lock();// 上读锁,其他线程只能读不能写,具有高并发性 try { System.out.println(Thread.currentThread().getName() + " read start."); Thread.sleep(1000L);// 模拟干活 System.out.println(Thread.currentThread().getName() + "read end."); } catch (InterruptedException e) { e.printStackTrace(); } finally { rwl.readLock().unlock(); // 释放写锁,最好放在finnaly里面 } } public void put() { rwl.writeLock().lock();// 上写锁,具有阻塞性 try { System.out.println(Thread.currentThread().getName() + " write start."); Thread.sleep(1000L);// 模拟干活 System.out.println(Thread.currentThread().getName() + "write end."); } catch (InterruptedException e) { e.printStackTrace(); } finally { rwl.writeLock().unlock(); // 释放写锁,最好放在finnaly里面 } } }
运行结果如下:
Thread-1 read start. Thread-0 read start. Thread-1read end. Thread-0read end. Thread-3 write start. Thread-3write end. Thread-2 write start. Thread-2write end.
从结果上面可以看的出来,读的时候是并发的,写的时候是有顺序的带阻塞机制的
例二
ReadLock和WriteLock的复杂使用情况,模拟一个有读写数据的场景
private final Map<String, Object> map = new HashMap<String, Object>();// 假设这里面存了数据缓存 private final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(); public Object readWrite(String id) { Object value = null; rwlock.readLock().lock();// 首先开启读锁,从缓存中去取 try { value = map.get(id); if (value == null) { // 如果缓存中没有数据,释放读锁,上写锁 rwlock.readLock().unlock(); rwlock.writeLock().lock(); try { if (value == null) { value = "aaa"; // 此时可以去数据库中查找,这里简单的模拟一下 } } finally { rwlock.writeLock().unlock(); // 释放写锁 } rwlock.readLock().lock(); // 然后再上读锁 } } finally { rwlock.readLock().unlock(); // 最后释放读锁 } return value; }
解读:
请一定要注意读写锁的获取与释放顺序。
源码解读类图如下:
其构造函数如下:
/** * Creates a new {@code ReentrantReadWriteLock} with * default (nonfair) ordering properties. */ public ReentrantReadWriteLock() { this(false); } /** * Creates a new {@code ReentrantReadWriteLock} with * the given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); }
解读:
读写锁的内部维护了一个 ReadLock 和一个 WriteLock,它们依赖 Sync 实现具体功能;Sync继承自 AQS,提供了公平和非公平的实现。
重点说明:
AQS 中只维护了一个 state 状态,而 ReentrantReadWriteLock 需要维护读状态和写状态,一个 state 怎么表示写和读两种状态呢?
ReentrantReadWriteLock 巧妙地使用 state 的高 16 位表示读状态,也即获取到读锁的次数;使用低 16 位表示获取到写锁的线程的可重入次数。
写锁
写锁使用 WriteLock 来实现
获取锁
对应代码如下:
/** * Acquires the write lock. * * <p>Acquires the write lock if neither the read nor write lock * are held by another thread * and returns immediately, setting the write lock hold count to * one. * * <p>If the current thread already holds the write lock then the * hold count is incremented by one and the method returns * immediately. * * <p>If the lock is held by another thread then the current * thread becomes disabled for thread scheduling purposes and * lies dormant until the write lock has been acquired, at which * time the write lock hold count is set to one. */ public void lock() { sync.acquire(1); }
解读:
类似于ReentrantLock的实现,实际上是调用了AbstractQueuedSynchronizer的acquire方法,代码如下:
/** * Acquires in exclusive mode, ignoring interrupts. Implemented * by invoking at least once {@link #tryAcquire}, * returning on success. Otherwise the thread is queued, possibly * repeatedly blocking and unblocking, invoking {@link * #tryAcquire} until success. This method can be used * to implement method {@link Lock#lock}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
重点关注子类中tryAcquire的实现
从上图可知,tryAcquire方法定义在Sync中,具体代码如下:
@ReservedStackAccess protected final boolean tryAcquire(int acquires) { /* * Walkthrough: * 1. If read count nonzero or write count nonzero * and owner is a different thread, fail. * 2. If count would saturate, fail. (This can only * happen if count is already nonzero.) * 3. Otherwise, this thread is eligible for lock if * it is either a reentrant acquire or * queue policy allows it. If so, update state * and set owner. */ Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
解读:
写锁是个独占锁,某个时刻只有一个线程可以获取该锁。
如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回;如果当前己经有线程获取到读锁或者写锁,则当前请求写锁的线程会被阻塞挂起。
对应上述代码分支:
(1)AQS状态值不为0
- 如果w==0则说明状态值的低16位为0,由于其高 16位不为 0,即表明己经有线程获取到读锁,所以直接返回 false。
- 如果w!=0则说明已经有线程获取到写锁,若当前线程不是该锁的持有者,则返回 false。
(2)AQS状态值为0
说明目前没有线程获取到读锁和写锁,对于 writerShouldBlock 方法,由Sync子类实现,公平锁和非公平锁的具体实现不一样。
/** * Nonfair version of Sync */ static final class NonfairSync extends Sync { private static final long serialVersionUID = -8159625535654395037L; final boolean writerShouldBlock() { return false; // writers can always barge } final boolean readerShouldBlock() { /* As a heuristic to avoid indefinite writer starvation, * block if the thread that momentarily appears to be head * of queue, if one exists, is a waiting writer. This is * only a probabilistic effect since a new reader will not * block if there is a waiting writer behind other enabled * readers that have not yet drained from the queue. */ return apparentlyFirstQueuedIsExclusive(); } }
解读:
对于非公平锁来说总是返回false,故如果抢占式执行CAS尝试获取写锁成功则设置当前锁的持有者为当前线程并返回 true,否则返回 false。
/** * Fair version of Sync */ static final class FairSync extends Sync { private static final long serialVersionUID = -2274990926593161451L; final boolean writerShouldBlock() { return hasQueuedPredecessors(); } final boolean readerShouldBlock() { return hasQueuedPredecessors(); } }
解读:
对于公平锁,其使用 hasQueuedPredecessors来判断当前线程节点是否有前驱节点,如果有则当前线程放弃获取写锁的权限,直接返回 false。
Note:
写锁是可重入锁,如果当前线程己经获取了该锁,再次获取时则只是简单地把可重入次数加 1 后直接返回。
释放锁
/** * Attempts to release this lock. * * <p>If the current thread is the holder of this lock then * the hold count is decremented. If the hold count is now * zero then the lock is released. If the current thread is * not the holder of this lock then {@link * IllegalMonitorStateException} is thrown. * * @throws IllegalMonitorStateException if the current thread does not * hold this lock */ public void unlock() { sync.release(1); }
解读:
类似于ReentrantLock的实现,实际上是调用了AbstractQueuedSynchronizer的release方法,而子类Sync只需要实现tryRelease方法即可,对应方法如下:
/* * Note that tryRelease and tryAcquire can be called by * Conditions. So it is possible that their arguments contain * both read and write holds that are all released during a * condition wait and re-established in tryAcquire. */ @ReservedStackAccess protected final boolean tryRelease(int releases) { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); setState(nextc); return free; }
解读:
如果当前线程持有锁,调用unlock方法会让持有的 AQS 状态值减 1,若减 1 后当前状态值为 0,则当前线程会释放锁,否则仅仅执行减 1。
如果当前线程没有持有锁,则调用该方法会抛出 IllegalMonitorStateException异常。
读锁
读锁是使用ReadLock来实现的。
获取锁
对应代码如下:
/** * Acquires the read lock. * * <p>Acquires the read lock if the write lock is not held by * another thread and returns immediately. * * <p>If the write lock is held by another thread then * the current thread becomes disabled for thread scheduling * purposes and lies dormant until the read lock has been acquired. */ public void lock() { sync.acquireShared(1); }
解读:
类似于ReentrantLock的实现,实际上是调用了AbstractQueuedSynchronizer的acquireShared方法,代码如下:
/** * Acquires in shared mode, ignoring interrupts. Implemented by * first invoking at least once {@link #tryAcquireShared}, * returning on success. Otherwise the thread is queued, possibly * repeatedly blocking and unblocking, invoking {@link * #tryAcquireShared} until success. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquireShared} but is otherwise uninterpreted * and can represent anything you like. */ public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
重点关注子类中tryAcquireShared的实现,即查看Sync中tryAcquireShared方法:
@ReservedStackAccess protected final int tryAcquireShared(int unused) { /* * Walkthrough: * 1. If write lock held by another thread, fail. * 2. Otherwise, this thread is eligible for * lock wrt state, so ask if it should block * because of queue policy. If not, try * to grant by CASing state and updating count. * Note that step does not check for reentrant * acquires, which is postponed to full version * to avoid having to check hold count in * the more typical non-reentrant case. * 3. If step 2 fails either because thread * apparently not eligible or CAS fails or count * saturated, chain to version with full retry loop. */ Thread current = Thread.currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; int r = sharedCount(c); if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != LockSupport.getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current); }
解读:
如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS 的状态值 state的高16位的值会增加 1,然后方法返回;
如果某个线程持有写锁,则当前线程会被阻塞。
对应上述代码分支:
(1)判断写锁是否被占用,如果是则直接返回 -1,而后调用 AQS 的 doAcquireShared方法把当前线程放入 AQS 阻塞队列。
(2)变量 r 是获取到读锁的个数
(3)readerShouldBlock方法,由Sync子类实现,公平锁和非公平锁的具体实现不一样;此处不展开描述,可以查看本文前面NonfairSync 和 FairSync的实现代码。
Note:
如果当前要获取读锁的线程己经持有了写锁,则也可以获取读锁;当其处理完事情,需要把读锁和写锁都释放掉,不能只释放写锁。
释放锁
对应代码如下:
/** * Attempts to release this lock. * * <p>If the number of readers is now zero then the lock * is made available for write lock attempts. If the current * thread does not hold this lock then {@link * IllegalMonitorStateException} is thrown. * * @throws IllegalMonitorStateException if the current thread * does not hold this lock */ public void unlock() { sync.releaseShared(1); }
解读:
类似于ReentrantLock的实现,实际上是调用了AbstractQueuedSynchronizer的releaseShared方法
/** * Releases in shared mode. Implemented by unblocking one or more * threads if {@link #tryReleaseShared} returns true. * * @param arg the release argument. This value is conveyed to * {@link #tryReleaseShared} but is otherwise uninterpreted * and can represent anything you like. * @return the value returned from {@link #tryReleaseShared} */ public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
子类Sync只需要实现tryReleaseShared方法即可,对应方法如下:
@ReservedStackAccess protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != LockSupport.getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // Releasing the read lock has no effect on readers, // but it may allow waiting writers to proceed if // both read and write locks are now free. return nextc == 0; } }
解读:
在无限循环里面,首先获取当前 AQS 状态值并将其保存到变量 c,然后变量 c 减去一个读计数单位后使用 CAS 操作更新 AQS 状态值。
如果更新成功则查看当前 AQS 状态值
- 当前AQS 状态值是否为 0,为 0 则说明当前己经没有读线程占用读锁,则 tryReleaseShared 返回 true(随后会调用 doReleaseShared 方法释放一个由于获取写锁而被阻塞的线程)。
- 当前 AQS 状态值不为 0,则说明当前还有其他线程持有了读锁,则 trγReleaseShared 返回 false。
Note:
关于无限循环的解释——如果 CAS 更新 AQS 状态值失败,则自旋重试直到成功。
ReentrantReadWriteLock与ReentrantLock的比较:
(1)相同点:都是一种显式锁,手动加锁和解锁,都很适合高并发场景
(2)不同点:ReentrantLock 是独占锁,ReentrantReadWriteLock是对ReentrantLock的复杂扩展,能适合更复杂的业务场景,ReentrantReadWriteLock可以实现一个方法中读写分离的锁机制。