package java.util.concurrent.locks;
ReadWriteLock接口
它保证:
只允许一个线程写入(其他线程既不能写入也不能读取);
没有写入时,多个线程允许同时读(提高性能)。
读的时候不能写 也就是说
读-写互斥,写-写互斥,读读不互斥
其实现类ReentrantReadWriteLock
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock(); //获取读锁
private final Lock wlock = rwlock.writeLock();//获取写锁
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}
把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,
适用于多读少写的场景
获取锁顺序
非公平模式(默认)
当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量
公平模式
当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比
写线程长,那么这组读线程组将会被分配读锁。
设置是否公平是通过构造,默认空参是不公平,有参false不公平,true公平
是可重入锁
个线程获取多少次锁,就必须释放多少次锁。这对于内置锁(synchronized)也是适用的
锁降级
从写锁变成读锁,该类支持锁降级
为什么需要锁降级呢,释放后在获取读锁读锁不行吗,
场景是:当获得了写锁,修改了数据后,先查看修改的数据,如果释放了锁,那么在多线程的情况下,有可能会有线程获得了写锁,
再次对数据进行了操作,那么我们就无法得知上次修改的值了,使用锁降级,没有释放锁,其他线程获取不到写锁,
然后在获取读锁,去读取数据,这样保证了数据被修改后,可以查看被修改的数据
锁升级
从读锁编程写锁,但是 ReentranlReadWriteLock是不支持锁升级的
读锁使用共享模式
写锁使用独占模式
ReentrantReadWriteLock实现的原理依然是内部类Sync继承AQS类,实现方式也是对state及waitStatus值进行操作。
实现读写锁需要分别记录读锁状态和写锁状态,并且等待列队中需要区别处理两种加锁操作
使用state值同时记录读写锁状态是将int类型的state变量分为高16位和低16位
读写状态的发生操作的就是state值高16位和低16位的改变,这样就类似于ReentrantLock,只是读的过程可以允许多个线程同时发生,
不需要加入到等待列队中,而写则依然需要加入等待列队
构造
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
//ReadLock是ReentrantReadWriteLock中的静态内部类,它是读取锁的实现
private final ReentrantReadWriteLock.ReadLock readerLock;
//WriteLock是ReentrantReadWriteLock中的静态内部类,它是写入锁的实现
private final ReentrantReadWriteLock.WriteLock writerLock;
//Sync是ReentrantReadWriteLock中的静态内部类,它继承了AQS
//它是读写锁实现的重点,后面深入分析
final Sync sync;
//默认使用非公平策略创建对象
public ReentrantReadWriteLock() {
this(false);
}
//fair置顶指定创建的锁是公平锁还是非公平锁
public ReentrantReadWriteLock(boolean fair) {
//FairSync和NonfairSync都继承自Sync,它们主要提供了对读写是否需要被阻塞的检查方法
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
}
Sync类是实现读写锁的重点,之前讲到的很多基于AQS实现的锁机制都会有一个内部类Sync这个类中
有不同的实现会有不同的功能我们看下读写锁中Sync内部的实现
//继承了AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
//常量值
static final int SHARED_SHIFT = 16;
//左移16位后,二进制值是10000000000000000,十进制值是65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//左移16位后再减一,十进制值是65535
//这个常量值用于标识最多支持65535个递归写入锁或65535个读取锁
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//左移16位后再减一,二进制值是1111111111111111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//用于计算持有读取锁的线程数
static int sharedCount(int c) {
//无符号右移动16位
//如果c是32位,无符号右移后,得到是高16位的值
return c >>> SHARED_SHIFT;
}
//用于计算写入锁的重入次数
static int exclusiveCount(int c) {
//如果c是32位,和1111111111111111做&运算,得到的低16位的值
return c & EXCLUSIVE_MASK;
}
//用于每个线程持有读取锁的计数
static final class HoldCounter {
//每个线程持有读取锁的计数
int count = 0;
//当前持有读取锁的线程ID
//这里使用线程ID而没有使用引用,避免垃圾收集器保留,导致无法回收
final long tid = Thread.currentThread().getId();
}
//通过ThreadLocal维护每个线程的HoldCounter
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
//这里重写了ThreadLocal的initialValue方法
public HoldCounter initialValue() {
return new HoldCounter();
}
}
//当前线程持有的可重入读取锁的数量,仅在构造方法和readObject方法中被初始化
//当持有锁的数量为0时,移除此对象
private transient ThreadLocalHoldCounter readHolds;
//成功获取读取锁的最近一个线程的计数
private transient HoldCounter cachedHoldCounter;
//第一个获得读锁的线程
private transient Thread firstReader = null;
//第一个获得读锁的线程持有读取锁的次数
private transient int firstReaderHoldCount;
Sync() {
//构建每个线程的HoldCounter
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
}
读锁的加锁
public void lock() {
sync.acquireShared(1);
}
//调用AQS的acquireShared(int arg)方法
public final void acquireShared(int arg) {
//tryAcquireShared(arg)获取读锁,如果获取失败即state值高16位的获取值小于0
//则证明在执行写入操作读锁应该是阻塞状态,需要将当前读线程加入到列队中去
if (tryAcquireShared(arg) < 0)
//加入到等待列队中
doAcquireShared(arg);
}
.。。。。。下面暂且。。。。。,资料不足
无锁状态下加锁
线程调用acquireShared函数,首先使用tryAcquireShared函数判断共享锁是否可获取成功,
由于当前为无锁状态则获取锁一定成功(如果同时多个线程在读锁进行竞争,则只有一个线程能够直接获取读锁,
其他线程需要进行fullTryAcquireShared函数继续进行锁的获取
有锁状态获取读锁
有锁状态则需要分情况讨论。其中需要分当前被持有的锁是读锁还是写锁,并且每种情况需要区分等待队列中是否有等待节点。
已有读锁且等待队列为空,
此时线程申请读锁,首先调用readerShouldBlock函数进行判断,该函数根据当前锁是否为公平锁判断规则稍有不同。
如果为非公平锁,则只需要当前第一个 等待节点不是写锁就可以尝试获取锁(考虑第一点为写锁主要为了防止写锁“饿死”);
如果是公平锁则只要有等待节点且当前锁不为重入就需要等待。
则当前线程使用CAS对读锁计数进行增加(同上文,如果同时多个线程在读锁进行竞争,则只有一个线程能够直接获取读锁,其他线程需要进入
fullTryAcquireShared函数继续进行锁的获取)。在成功对读锁计数器进行增加后,当前线程需要继续对当前线程持有读锁的计数进行增加
此时分为两种情况:
1当前线程是第一个获取读锁的线程,此时由于第一个获取读锁的线程已经通过firstReader及firstReaderHoldCount两个变量进行存储,
则仅仅需要将firstReaderHoldCount加1即可;
2 当前线程不是第一个获取读锁的线程,则需要使用readHolds进行存储,readHolds是ThreadLocal的子类,
通过readHolds可获取当前线程对应的HoldCounter类的对象,该对象保存了当前线程获取读锁的计数。考虑程序的局部性原理,
又使用cachedHoldCounter缓存最近使用的HoldCounter类的对象,如在一段时间内只有一个线程请求读锁则可加速对读锁获取的计数。
部分内容来自 https://zhuanlan.zhihu.com/p/87590807