ReentrantLock
是独占锁, 某时只有一个线程可以获取该锁,而实际中会有写少读多的场景,ReentrantReadWriteLock
应运而生。ReentrantReadWriteLock
采用读写分离的策略,允许多个线程可以同时获取读锁。
读写锁的内部维护了一个ReadLock
和一个WriteLock
,它们依赖Sync 实现具体功能。
state的含义 ReentrantReadWriteLock
需要维护读状态和写状态, 为了用一个state 表示写和读两种状态,ReentrantReadWriteLock
巧妙地使用state
的高16位表示读状态 ,也就是获取到读锁的次数;使用低16 位表示获取到写锁的线程的可重入次数 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static final int SHARED_SHIFT = 16 ;static final int SHARED_UNIT = (1 << SHARED_SHIFT);static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1 ;static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1 ;static int sharedCount (int c) { return c >>> SHARED_SHIFT; }static int exclusiveCount (int c) { return c & EXCLUSIVE_MASK; }static final class HoldCounter { int count = 0 ; final long tid = getThreadId(Thread.currentThread()); }
写锁 写锁的获取 写锁是个独占锁 , 某时只有一个线程可以获取该锁。如果当前没有线程获取到读锁和写锁, 则当前线程可以获取到写锁然后返回。如果当前己经有线程获取到读锁和写锁,则当前请求写锁的线程会被阻塞挂起。另外, 写锁是可重入锁,如果当前线程己经获取了该锁,再次获取只是简单地把可重入次数加1后直接返回。
1 2 3 public void lock () { sync.acquire(1 ); }
其中重写的tryAcquire
方法的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 protected final boolean tryAcquire (int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0 ) { if (w == 0 || current != getExclusiveOwnerThread()) return false ; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error ("Maximum lock count exceeded" ); setState(c + acquires); return true ; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false ; setExclusiveOwnerThread(current); return true ; }
c
是AQS状态变量,w
是c
的低16位(写状态)。
c!=0&&w==0
时说明状态值的的高16位(读状态)不为0,这说明了已经有线程获取了读锁,返回false
。
c!=0&&w!=0&¤t!= getExclusiveOwnerThread()
则说明当前已经有线程获取了写锁,但是当前线程不是写锁的持有者,返回false
。
写锁的释放 如果当前线程持有写锁,调用锁释放方法会让该线程对该线程持有的AQS状态值减1 ,如果减去1后当前状态值为0 则当前线程会释放该锁, 否则仅仅减1而己。其中重写的tryRelease
方法如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 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; }
读锁 读锁的获取 写锁是共享锁。获取读锁,如果当前没有其他线程持有写锁 ,则当前线程可以获取读锁, AQS 的状态值state 的高16 位的值会增加1 ,然后方法返回。否则如果其他一个线程持有写锁, 则当前线程会被阻塞。共享锁的tryAcquireShared
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 protected final int tryAcquireShared (int unused) { 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 != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0 ) readHolds.set(rh); rh.count++; } return 1 ; } return fullTryAcquireShared(current); }
如上代码首先查看是否有其他线程获取到了写锁,如果是则直接返回-1。
如果当前要获取读锁的线程己经持有了写锁, 则也可以获取读锁。但是需要注意,当一个线程先获取了写锁,然后获取了读锁处理事情完毕后,要记得把读锁和写锁都释放掉,不能只释放写锁。
在线程尝试获取读锁前,会进行readerShouldBlock
检查,其中非公平锁的readerShouldBlock
实现代码如下,代码的作用是检查队列的第一个元素是否存在以及是不是正在尝试获取写锁。
1 2 3 final boolean readerShouldBlock () { return apparentlyFirstQueuedIsExclusive(); }
1 2 3 4 5 6 7 8 final boolean apparentlyFirstQueuedIsExclusive () { Node h, s; return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null ; }
进行readerShouldBlock
检查后,还要检查获取读锁的线程是否达到了最大值,然后才是执行CAS 操作将AQS 状态值的高16 位值增加1。
更新AQS状态值后,最后还要更新一些重要的变量:
firstReader
:用来记录第一个获取到读锁的线程
firstReaderHoldCount
:记录第一个获取到读锁的线程获取读锁的可重入次数
cachedHoldCounter
:记录最后一个获取读锁的线程获取读锁的可重入次数
readHolds
:ThreadLocal 变量, 用来存放除去第一个获取读锁线程外的其他线程获取读锁的可重入次数
fullTryAcquireShared
类似tryAcquireShared
,但是是自旋获取。
1 2 3 4 5 6 7 8 9 10 11 final int fullTryAcquireShared (Thread current) { HoldCounter rh = null ; for (;;) { if (compareAndSetState(c, c + SHARED_UNIT)) { return 1 ; } }
读锁的释放 tryReleaseShared
代码如下:
1 2 3 4 5 6 7 8 9 10 11 protected final boolean tryReleaseShared (int unused) { Thread current = Thread.currentThread(); for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) return nextc == 0 ; } }
在无限循环里面,首先获取当前AQS 状态值并将其保存到变量c ,然后变量c被减去一个读计数单位后使用CAS 操作更新AQS 状态值,如果更新成功则查看当前AQS 状态值是否为0 ,为0则说明当前己经没有读线程占用读锁,则tryReleaseShared
返回true 。然后会调用doReleaseShared
方法释放一个由于获取写锁而被阻塞的线程 ,如果当前AQS 状态值不为0 ,则说明当前还有其他线程持有了读锁,所以trγReleaseShared
返回false 。如果tryReaseShared
中的CAS 更新AQS 状态值失败,则自旋重试直到成功。
读写锁使用案例 改造List,使得在读多写少的情况下保持性能和线程安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class SecureList <T> { List<T> list=new ArrayList <>(); final ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock (); final Lock writeLock=reentrantReadWriteLock.writeLock(); final Lock readLock=reentrantReadWriteLock.readLock(); T get (int index) { readLock.lock(); try { return list.get(index); } finally { readLock.unlock(); } } void set (int index,T ele) { writeLock.lock(); try { list.set(index,ele); } finally { writeLock.unlock(); } } void remove (int index) { writeLock.lock(); try { list.remove(index); } finally { writeLock.unlock(); } } }
读写锁的改进 StampedLock
是并发包里面JDK8 版本新增的一个锁,该锁提供了三种模式的读写控制,当调用获取锁的系列函数时,会返回一个long 型的变量,我们称之为戳记( stamp),这个戳记代表了锁的状态。其中t可系列获取锁的函数,当获取锁失败后会返回为0的stamp 值。当调用释放锁和转换锁的方法时需要传入获取锁时返回的stamp 值。
StampedLock
提供的三种读写模式的锁分别如下:
写锁writeLock
:独占锁。
悲观读锁readLock
:悲观是指在具体操作数据前其会悲观地认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在读少写多的情况下的一种考虑。
乐观读锁tryOptimisticRead
:它是相对于悲观锁来说的,该锁的一个特点是适用于读多写少的场景, 因为获取读锁只是使用位操作进行检验,不涉及CAS 操作,所以效率会高很多,但是同时由于没有使用真正的锁,所以返回的不是最新的数据,但是一致性还是得到保障的。