读写锁
独占锁X:指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Synchronized 而言都是独占锁
共享锁S:指该锁可以被多个线程锁持有
ReentrantReadWriteLock 其读锁是共享锁,写锁是独占锁
作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写
使用规则:
-
加锁解锁格式:
r.lock(); try { // 临界区 } finally { r.unlock(); }
-
读-读能共存、读-写不能共存、写-写不能共存【读锁保护数据的 read() 方法,写锁保护数据的 write() 方法】不同线程间读写是互斥的
-
读锁不支持条件变量
-
升级:获取读锁的情况下还想获取写锁; 降级:先获取写锁在获取读锁
-
重入时升级不支持:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写【必须先释放掉读锁】
-
【可能有多个线程在读,如果其中一个线程想升级,那其他的读线程就 会 很 难 办】
-
【可以这样理解:因为读锁之间时兼容的,当前线程获得读锁的同时,其他线程可能也获得了读锁】
-
-
重入时降级支持 :持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁【写的时候能读,因为写锁只能被一个线程获取】
-
在保证数据一致性的同时,尽量减少持有写锁的时间
-
可以做到,因为此线程拿到写锁,只有本线程可以去申请读锁,其他线程拿不到锁了,因为读写不共存。源码中有判断是否时当前线程的if判断。
- 见w.lock : c != 0 and w == 0 表示有读锁(之前有的),【读锁不能升级】,直接返回 false====从这里也能看出不能升级,也就是原先有了读锁还要尝试tryAcquire获取写锁。(不允许)
w.lock(); try { r.lock();// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存 try { // ... } finally{ w.unlock();// 要在写锁释放之前获取读锁 } } finally{ r.unlock(); }
-
在同一个线程内部,情况则有所不同。对于ReentrantReadWriteLock这样的可重入读写锁来说,一个线程是可以先获取独占锁(写锁),然后在不释放该锁的情况下再次获取共享锁(读锁)的。这种情况下,线程仍然持有独占锁,但同时也可以进行读操作(因为它也持有了共享锁)。但请注意,这并不意味着独占锁和共享锁在同一时刻"共存"于同一个线程中,而是线程在内部以特定的方式管理了这两种锁的状态。
构造方法:
-
public ReentrantReadWriteLock()
:默认构造方法,非公平锁 -
public ReentrantReadWriteLock(boolean fair)
:true 为公平锁
常用API:
-
public ReentrantReadWriteLock.ReadLock readLock()
:返回读锁 -
public ReentrantReadWriteLock.WriteLock writeLock()
:返回写锁 -
public void lock()
:加锁 -
public void unlock()
:解锁 -
public boolean tryLock()
:尝试获取锁
读读并发:
public static void main(String[] args) {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock r = rw.readLock();
ReentrantReadWriteLock.WriteLock w = rw.writeLock();
new Thread(() -> {
r.lock();
try {
Thread.sleep(2000);
System.out.println("Thread 1 running " + new Date());
} finally {
r.unlock();
}
},"t1").start();
new Thread(() -> {
r.lock();
try {
Thread.sleep(2000);
System.out.println("Thread 2 running " + new Date());
} finally {
r.unlock();
}
},"t2").start();
}
缓存应用
用读写锁实现既能保证一致性,又能不像加普通锁那样性能降低
缓存更新时,是先清缓存还是先更新数据库
-
先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新过期数据到缓存
-
先更新据库 :可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据【 但多做一次查询时可以把错纠正过来 】
-
补充情况:查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询
可以使用读写锁进行操作
成员属性原理
读写锁用的是同一个 Sycn(AQS) 同步器,因此等待队列、state 等也是同一个 ,原理与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位【 state分成了两份,还是0--无锁 】
-
读写锁:
private final ReentrantReadWriteLock.ReadLock readerLock; private final ReentrantReadWriteLock.WriteLock writerLock;
-
构造方法:默认是非公平锁,可以指定参数创建公平锁
public ReentrantReadWriteLock(boolean fair) { // true 为公平锁 sync = fair ? new FairSync() : new NonfairSync(); // 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现 readerLock = new ReadLock(this); writerLock = new WriteLock(this); }
Sync 类的属性:
-
统计变量:
// 用来移位 static final int SHARED_SHIFT = 16; // 高16位的1 static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 65535,16个1,代表写锁的最大重入次数 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 低16位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数 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; }
-
内部类:
// 记录读锁线程自己的持有读锁的数量(重入次数),因为 state 高16位记录的是全局范围内所有的读线程获取读锁的总量 static final class HoldCounter { int count = 0; // Use id, not reference, to avoid garbage retention final long tid = getThreadId(Thread.currentThread()); } // 线程安全的存放线程各自的 HoldCounter 对象 static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); } }
-
内部类实例:
// 当前线程持有的可重入读锁的数量,计数为 0 时删除 private transient ThreadLocalHoldCounter readHolds; // 记录最后一个获取【读锁】线程的 HoldCounter 对象 private transient HoldCounter cachedHoldCounter;
-
首次获取锁:
// 第一个获取读锁的线程 private transient Thread firstReader = null; // 记录该线程持有的读锁次数(读锁重入次数) private transient int firstReaderHoldCount;
-
Sync 构造方法:
Sync() { readHolds = new ThreadLocalHoldCounter(); // 确保其他线程的数据可见性,state 是 volatile 修饰的变量,重写该值会将线程本地缓存数据【同步至主存】 setState(getState()); }