读写锁探究
介绍
- 读写锁ReadWriteLock 管理两种锁,包括只读的锁和写锁
- 读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占锁
- 所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
- 读写锁的特性是:写独占,读共享,读写锁是独占锁和共享锁的结合体
ReadWriteLock
介绍
-
ReadWriteLock是一个接口
-
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
-
接口中规定了读锁和写锁的方法定义,返回类型是Lock
-
有一个实现类ReentrantReadWriteLock
ReentrantReadWriteLock
简单使用
-
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); ReadLock readLock = rwLock.readLock(); WriteLock writeLock = rwLock.writeLock(); public void read() { readLock.lock(); try { System.err.println(Thread.currentThread().getName() + " 进入了读方法。。。"); Thread.sleep(3000); System.err.println(Thread.currentThread().getName() + " 退出了读方法。。。"); } catch (Exception e) { e.printStackTrace(); } finally { readLock.unlock(); } } public void write() { writeLock.lock(); try { System.err.println(Thread.currentThread().getName() + " 进入了写方法。。。"); Thread.sleep(3000); System.err.println(Thread.currentThread().getName() + " 执行了写方法。。。"); } catch (Exception e) { e.printStackTrace(); } finally { writeLock.unlock(); } } public static void main(String[] args) { TestLock07 urwLock = new TestLock07(); IntStream.of(1, 2).forEach(value -> { new Thread(urwLock::read, "读线程" + value).start(); new Thread(urwLock::write, "写线程" + value).start(); }); }
-
从结果可以看出 读锁是允许多个线程同时获取的,写锁不行
构造器
-
public ReentrantReadWriteLock() { this(false); }
-
public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); }
-
构造器提供了写锁和读锁实例,默认是非公平锁,可以通过参数进行控制
内部类
Sync
四个常量
-
读写锁中 AQS的state被分成了两部分,高16位表示读的次数,低16位表示写锁的次数
-
//用于读锁的自增 static final int SHARED_SHIFT = 16; //1<<16 是65536,每次获取读锁成功state都会加65536,代表把值加到高16位上 static final int SHARED_UNIT = (1 << SHARED_SHIFT); //允许读或写获取锁的最大次数,都是65535 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; //用来获取独占锁加锁次数 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
两个内部类
-
HoldCounter
static final class HoldCounter { int count = 0; final long tid = getThreadId(Thread.currentThread()); }
用于存放每个线程持有的读锁数量,包括可重入锁,每一个线程都会对应一个HoldCounter的实例,其中count保存线程持有的读锁的数量,tid表示这个线程的id,防止被回收
不会存储第一个加锁线程
-
ThreadLocalHoldCounter
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); } }
用于返回一个HoldCounter实例
四个变量
-
private transient ThreadLocalHoldCounter readHolds; //ThreadLocalHoldCounter的实例
-
private transient HoldCounter cachedHoldCounter; //最后一次加锁线程的HoldCounter实例
-
private transient Thread firstReader = null; //第一个获取到读锁的线程 firstReader是最后一次将共享计数从0更改为1的唯一线程 如果没有这样的线程那么这个 值为null
-
private transient int firstReaderHoldCount; //firstReader线程持有锁的数量
两个方法
-
//获取当前读锁的总数 static int sharedCount(int c) { return c >>> SHARED_SHIFT; } //获取当前写锁的总数 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
-
c代表当前state的值
-
c >>> SHARED_SHIFT; 代表当前的state无符号右移,拿到的值就是读锁上锁的次数
-
c & EXCLUSIVE_MASK; EXCLUSIVE_MASK的值为65535,和当前state进行&操作会得到state低16位的值
构造器
-
Sync() { //把当前线程加了多少次可重入锁放入一个ThreadLocal中 readHolds = new ThreadLocalHoldCounter(); //看来只是为了初始化readHolds setState(getState()); }
NonfairSync
writerShouldBlock()
-
final boolean writerShouldBlock() { return false; }
直接返回false
readerShouldBlock()
-
final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); }
-
final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; //(h = head) != null 同步队列存在 //(s = h.next) != null 同步队列不为空 //!s.isShared() 等待队列中下一个节点不是share节点,从这里可以看出来,如果头结点的后继节点是写节点,那么这里一定会阻塞,一个写节点会阻塞后续的所有读节点,这样做是为了防止写锁饥饿 //s.thread != null 这个节点具有实际意义,Thread不是null //最终 h指向head s指向head.next //符合以上四种情况才会返回 true 即在当前的同步队列中只要头结点的后继节点是状态小于0的写节点 就会返回true return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null; }
FairSync
writerShouldBlock()
-
final boolean writerShouldBlock() { return hasQueuedPredecessors(); }
通过hasQueuedPredecessors()进行判断
readerShouldBlock()
-
final boolean readerShouldBlock() { return hasQueuedPredecessors(); }
ReadSync
-
Lock的实现类
-
是对Sync中共享方式获取锁的再一次包装
WriteSync
-
Lock的实现类
-
是对Sync中独占模式获取锁的再一次包装
获取锁
tryAcquire
-
独占锁模式尝试加锁 如果有读线程和写线程持有锁且非当前线程,则失败,如果计数饱和,则失败
-
protected final boolean tryAcquire(int acquires) { //获取当前线程 Thread current = Thread.currentThread(); //state值 int c = getState(); //独占锁加锁次数,包括可重入锁 int w = exclusiveCount(c); //c!=0 说明已经有加锁了,可能是读锁也可能是写锁+ if (c != 0) { //正常的加锁流程,w=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; } //c=0情况,说明还没有加过锁,如果不能直接抢锁则writerShouldBlock方法会返回true,方法结束 //如果可以直接抢锁则会通过CAS获取锁,成功后修改独占锁变量为当前线程,返回成功 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
-
关于writerShouldBlock()这个方法在公平锁和非公平锁中都有不同的实现,可以看出它是用来判断是应该直接获取锁还是应该去排队的。
tryAcquireShared
-
共享锁模式尝试加锁
-
protected final int tryAcquireShared(int unused) { //获取当前线程 Thread current = Thread.currentThread(); //state值 int c = getState(); //查看独占锁的加锁状态 //当exclusiveCount(c) != 0 即已经加了独占锁,并且加锁线程不是当前线程时,返回-1, 加锁失败 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; //共享锁加锁次数 int r = sharedCount(c); //readerShouldBlock()用于判断当前节点是否应该被阻塞 //r < MAX_COUNT 边界 //compareAndSetState(c, c + SHARED_UNIT) CAS去修改节点的值 SHARED_UNIT 为 1<<16 代表加一次读锁 //以上全部为true后->加锁成功 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { //读锁加锁次数为0,还没有加过读锁 if (r == 0) { //这是第一个加读锁的 firstReader = current; //加锁数为1 firstReaderHoldCount = 1; } else if (firstReader == current) { //不是加的一个锁,但是第一个锁也是我加的 firstReaderHoldCount++; } else { //既不是加的第一个锁,第一个加锁的也不是我 //获取HoldCounter实例,取得是最后一个读锁的线程计数器, HoldCounter rh = cachedHoldCounter; //判断当前线程是不是之前最后一次加锁的线程,如果不是才会初始化一个新的HoldCounter实例,算是一个小优化 if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) //这个线程第一次加锁,添加当前线程的HoldCounter实例信息到相应的 readHolds.set(rh); //线程加可重入锁 直接 count++ rh.count++; } return 1; } //如果没有拿到锁 return fullTryAcquireShared(current); }
针对每一个线程都有一个ThreadLocal的引用,每个线程的ThreadLocal都是不同的,在这里的HoldCounter是通过一个继承了ThreadLocal的内部类ThreadLocalHoldCounter来初始化的,所以针对每一个Thread 都有一个属于自己的HoldCounter来存储自己加锁的次数
还有 cachedHoldCounter 代表最后一次 加锁的线程的HoldCounter 信息,算是一种优化,在大部分情况下,重入和释放锁的线程很有可能就是最后一次加锁的线程 ,单独拿出来会省去一部分判断
fullTryAcquireShared()
-
//这个方法发生在读锁获取锁失败后,这里的代码和TryAcquireShared有着很大的相似性,因为多线程系统中线程的执行有着很大的不确定性,所以部分获取锁的过程需要重新走一遍 final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) { int c = getState(); if (exclusiveCount(c) != 0) { //查看独占锁,如果存在独占锁 并且不是当前线程的,获取锁失败 if (getExclusiveOwnerThread() != current) return -1; //判断当前线程是否可以获取锁 } else if (readerShouldBlock()) { if (firstReader == current) { } else { //第一个加读锁的线程不是当前线程的话 //第一次循环的时候 rh必定是null if (rh == null) { //指向当前实例的 cachedHoldCounter rh = cachedHoldCounter; //在ThreadLocalHoldCounter中获取属于当前线程的读锁加锁信息 if (rh == null || rh.tid != getThreadId(current)) { rh = readHolds.get(); if (rh.count == 0) readHolds.remove(); } } if (rh.count == 0) return -1; } } if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); //CAS 获取锁成功后 if (compareAndSetState(c, c + SHARED_UNIT)) { //是否第一个获取读锁 if (sharedCount(c) == 0) firstReader = current; firstReaderHoldCount = 1; //第一个获取读锁的是不是我 } else if (firstReader == current) { firstReaderHoldCount++; //其他情况 } else { //rh代表当前线程的加锁信息, HoldCounter实例 if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) //重新初始化一个 HoldCounter实例 rh = readHolds.get(); else if (rh.count == 0) //这里的rh是已经存在的情况下,或者是多次遍历后的结果 readHolds.set(rh); rh.count++; //最后一个加锁的线程计数 匹配 rh最后的值 获取读锁及后续处理成功 cachedHoldCounter = rh; } return 1; } } }
写锁加锁过程
-
写锁有公平锁和非公平锁两种
-
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); public void write() { writeLock.lock(); try { System.err.println(Thread.currentThread().getName() + " 进入了写方法。。。"); Thread.sleep(10000); System.err.println(Thread.currentThread().getName() + " 执行了写方法。。。"); } catch (Exception e) { e.printStackTrace(); } finally { writeLock.unlock(); } } public static void main(String[] args) { TestLock08 urwLock = new TestLock08(); new Thread(urwLock::write, "写线程").start(); }
-
writeLock调用了lock()方法,在lcok() 方法中 :
sync.acquire(1);
,直接调用了Sync中的acquire()方法,参数是1表示加锁一次。 -
在Sync类中。并没有重写父类AQS的acquire(),实际上这个方法还是AQS提供的,代码如下:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
调用tryAcquire(arg)尝试加锁,arg的值为1
- 针对这个方法,简单的说就是 如果有读线程和写线程持有锁且非当前线程,则失败,如果计数饱和,则失败。
- 公平锁和非公平锁的差异在writerShouldBlock()这个方法上,在公平锁和非公平锁的子类中都有不同的实现
- 非公平锁中直接返回false,在tryAcquire(arg)方法这里是||连接,前面是false才会执行后面的CAS方法修改state的值,CAS前面使用取反,所以获取失败后的结果为false取反为true,两者有一个为true执行if块中的方法,获取锁失败
- 公平锁中,会执行hasQueuedPredecessors()方法,判断有没有现成排队,如果有线程节点在等待队列中排队,那么当前线程就不应该拿锁
- 获取锁成功后会修改Exclusive,也就是独占线程,返回true。
-
!tryAcquire(arg) 在获取锁成功后的值就是false,会短路后面的acquireQueued()方法,整个方法结束,线程开始执行下面的任务。
-
如果获取锁失败了,那么就会进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法,先开始执行addWaiter(Node.EXCLUSIVE),参数是一个独占节点
- addWaiter()方法中会创建一个节点,在这里是独占类型的node。
- 创建完成后,插入同步队列的尾部,返回这个新创建的node。
-
之后会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg),第一个参数是新的node,第二个参数是本次加锁次数1
- 方法中包含了一个循环,如果当前节点是首节点的后继,就会调用tryAcquire(arg)尝试加锁,加锁成功后,把当前node设为首节点,node成为虚拟节点,返回中断标志位
- 获取锁失败的话,需要尝试让线程等待一段时间,不再占用资源,等待的底层是park()方法。执行park()需要等待unpark()方法来唤醒当前线程
- 如果在park过程中当前线程被其他中断,则会改变线程的中断标志位,但是在获取锁阶段不会去处理中断,获取锁成功后才会根据线程标志位处理中断
-
总结:写锁首先会尝试获取锁,如果锁不是自己持有或者计数器已满,则获取锁失败,生成node进入同步队列,之后不断循环获取锁,一定次数后把node放入等待队列,等待被唤醒,获得锁后查看中断标志处理中断
读锁加锁过程
-
读锁有公平锁和非公平锁两种
-
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); public void read() { readLock.lock(); try { System.err.println(Thread.currentThread().getName() + " 进入了读方法。。。"); Thread.sleep(3000); System.err.println(Thread.currentThread().getName() + " 退出了读方法。。。"); } catch (Exception e) { e.printStackTrace(); } finally { readLock.unlock(); } } public static void main(String[] args) { TestLock08 urwLock = new TestLock08(); new Thread(urwLock::read, "读线程").start(); }
-
因为读锁是一种共享锁,所以在加读锁的时候是这样的:
sync.acquireShared(1);
。 -
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
-
读锁和写锁是有本质差距的,读锁和读锁是不互斥的,但是写锁和任何锁互斥,读锁加锁的时候要查看当前加锁的是到底是什么锁,tryAcquireShared(arg)尝试加共享锁,参数为1
- 概括这个方法:不是当前锁加的写锁或者计数器饱和,加锁失败,加锁还是需要CAS的,并不一定成功
- 公平锁和非公平锁的差异在readerShouldBlock()方法
- 公平锁的情况读锁和写锁是一样的,都需要查看等待队列中是否有线程在排队
- 非公平锁下有一种情况是不能加读锁的,在等待队列中首节点的后继是一个有效的独占节点,不能直接获取锁。
- 就是在 读->写的情况下读线程正在持有,写线程正在等待,这种情况下再来的读线程不能获取锁,需要去等待。为的是避免写锁饥饿
- CAS加锁成功后,如果是第一次加读锁或者是第一个加读锁的线程加锁,就直接修改
firstReader
和firstReaderHoldCount
。 - 都不是的话就去获取
cacheHoldCounter
看看是不是当前线程,不是当前线程或者是null就创建一个Holdcounter实例,也就是readHolds.get()并赋值给cacheHoldCounter
,其中的count++
-
CAS加锁失败就会执行fullTryAcquireShared(current),参数为当前线程
- 循环获取锁直到return
- 公平锁情况如果当前持有线程的线程是写线程或者等待队列中存在线程等待,不允许获取锁
- 非公平锁情况如果当前持有锁的线程是写线程,如果当前等待队列首节点的后继是写节点 并且 当前线程不是第一次加锁的线程 不允许获取锁
- 以上情况直接返回 -1。
- 其他情况就是CAS获取锁,成功后修改
firstReader
和firstReaderHoldCount
或者cacheHoldCounter
,在这还要为cacheHoldCounter
赋值为当前线程的HoldCounter,因为当前线程是最后一个加锁的
-
当tryAcquireShared(arg)返回-1时,会进入doAcquireShared(arg)方法
- 生成一个节点,放入同步队列。循环调用tryAcquireShared(arg),直到获取成功,将节点设置为头节点,这里的头节点的状态有可能会是 -3
- 不断获取锁失败后需要让线程进入等待不再消耗资源,调用park(),等待unpark()唤醒,被唤醒后先抢锁,抢锁成功后查看是否需要处理中断
-
总结:首先尝试获取读锁,如果当前线程正持有写锁且不是本线程获取锁失败,成node进入同步队列,之后不断循环获取锁,一定次数后把node放入等待队列,等待被唤醒,获得锁后查看中断标志处理中断
读锁加锁流程图
tryLock()
- ReadLock 中,
return sync.tryReadLock();
- WriteLock中,
return sync.tryWriteLock();
- 这两种只是尝试性的加锁,加上了就持有锁,加不上就算了,不会去循环死等,也不会去管等待队列中正在等待的node。在写锁和读锁中,这个方法没有对公平锁和非公平锁的区别
tryWriterLock()
-
final boolean tryWriteLock() { //获取当前线程 Thread current = Thread.currentThread(); int c = getState(); //已经加了锁的情况 if (c != 0) { //写锁的加锁状态 int w = exclusiveCount(c); //w==0 说明加了读锁。不能再加写锁了 //current != getExclusiveOwnerThread() 说明独占锁的不是本线程 if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w == MAX_COUNT) throw new Error("Maximum lock count exceeded"); } //CAS获取锁成功后修改独占锁为当前线程 if (!compareAndSetState(c, c + 1)) return false; setExclusiveOwnerThread(current); return true; }
-
相对tryAcquire()来说缺少了writerShouldBlock方法进行判断
tryReadLock()
-
相对于写锁来说也是比较复杂的,相对于tryAcquireShared也是少对readerShouldBlock的调用
-
final boolean tryReadLock() { Thread current = Thread.currentThread(); for (;;) { int c = getState(); //读锁之前有没有加写锁,有写锁就无法加读锁 //独占锁的线程不是本线程也无法加读锁 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return false; int r = sharedCount(c); if (r == MAX_COUNT) throw new Error("Maximum lock count exceeded"); //CAS去修改state的值 if (compareAndSetState(c, c + SHARED_UNIT)) { //和tryAcquireShared的处理类似 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 true; } } }
释放锁
tryRelease()
-
释放锁的过程比较简单
-
//releases,释放重入次数,一般为1 protected final boolean tryRelease(int releases) { //判断独占线程的是不是我 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; //判断写锁是否已经释放干净 boolean free = exclusiveCount(nextc) == 0; if (free) //独占线程设为null setExclusiveOwnerThread(null); setState(nextc); return free; }
protected final boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); }
tryReleaseShared()
-
protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); //是第一个线程的话 if (firstReader == current) { if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != 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)) //nextc等于0的话 就需要去唤醒后面等待的写线程,如果不是0,还有线程占有读锁不需要唤醒后续的写线程 return nextc == 0; } }
锁释放过程
写锁
-
sync.release(1);
实际调用的AQS实现的release()方法 -
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
简单的说,释放锁,释放后如果不在占有则唤醒后面的线程node
读锁
-
sync.releaseShared(1);
-
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
唤醒的时候有一个循环,这里要注意在读锁后面阻塞的只能是写锁,唤醒写线程后等写线程运行完成会唤醒后面的读线程(假设这里是读线程),读线程被唤醒去获取锁,成功后在doAcquireShared()方法中会调用setHeadAndPropagate()
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; //唤醒下一个线程node,如果他是读node的话 if (s == null || s.isShared()) doReleaseShared(); } }