ReentrantReadWriteLock(读写锁)
读写锁概述
ReentrantReadWriteLock,读写锁或者重入读写锁,它维护了一个读锁和一个写锁,可以达到多个读线程可以共享的获取到锁,而此时写线程不能获取到锁,并且当写线程获取到锁时后续的读写都将被阻塞不能获取到锁。读写锁保证了写操作对后续的读操作的可见性。同时ReentrantReadWriteLock还支持重入,公平性选择以及锁的降级(获取写锁,获取读锁之后释放写锁,写锁能降级为读锁)。
ReadWriteLock接口
ReadWriteLock接口定义很简单,分别返回两个锁,分别为读锁和写锁。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock示例
下面的例子演示了一个线程像List里面添加10个消息,添加消息的代码用写锁同步,另外三个线程不停的去取List中的最新的一条消息,取的操作用读锁保护。
public class ReentratReadWriteLockDemo {
public static void main(String[] args) {
News news = new News();
//read
for(int n = 0; n < 3; n++){
new Thread(new Runnable() {
@Override
public void run() {
String pre = "";
while(true){
String s = news.getLast();
if(s == null)
continue;
if(!s.equals(pre)) {
pre = s;
System.out.println(Thread.currentThread().getName() + " get the last news : " + s);
if(Integer.parseInt(s) == 9)
break;
}
}
}
}, "read thread" + n).start();
}
//write
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 10; i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
news.add(i + "");
}
}
}).start();
}
static class News {
private final List<String> newsList = new ArrayList<>();
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
public String getLast(){
readLock.lock();
try{
if(newsList.size() == 0)
return null;
return newsList.get(newsList.size() - 1);
}
finally {
readLock.unlock();
}
}
public void add(String news) {
writeLock.lock();
try{
newsList.add(news);
System.out.println("add a news:" + news);
}
finally {
writeLock.unlock();
}
}
}
}
运行结果:
add a news:0
read thread1 get the last news : 0
read thread2 get the last news : 0
read thread0 get the last news : 0
add a news:1
read thread2 get the last news : 1
read thread0 get the last news : 1
read thread1 get the last news : 1
add a news:2
read thread2 get the last news : 2
read thread1 get the last news : 2
read thread0 get the last news : 2
add a news:3
read thread2 get the last news : 3
read thread1 get the last news : 3
read thread0 get the last news : 3
add a news:4
read thread0 get the last news : 4
read thread1 get the last news : 4
read thread2 get the last news : 4
add a news:5
read thread2 get the last news : 5
read thread0 get the last news : 5
read thread1 get the last news : 5
add a news:6
read thread0 get the last news : 6
read thread2 get the last news : 6
read thread1 get the last news : 6
add a news:7
read thread0 get the last news : 7
read thread2 get the last news : 7
read thread1 get the last news : 7
add a news:8
read thread1 get the last news : 8
read thread2 get the last news : 8
read thread0 get the last news : 8
add a news:9
read thread1 get the last news : 9
read thread0 get the last news : 9
read thread2 get the last news : 9
从输出结果可以很直观的看到首先没有脏数据,然后写的操作对于读是可见的。
ReentantReadWriteLock的读写状态设计
首先对于同步状态,或者说同步资源来说,在AQS里面维护的其实就是一个int型的变量state,state的值可以表示获取到同步状态的线程的数量。因此重入锁可以简单的以重入则增加state的值的形式实现。
对于ReentantReadWriteLock因为要维护两个锁(读/写),但是同步状态state只有一个,所以ReentantReadWriteLock采用“按位切割”的方式,所谓“按位切割”就是将这个32位的int型state变量分为高16位和低16位来使用,高16位代表读状态,低16位代表写状态。
高16位 低16位
读状态 写状态
------------------- -------------------
0000 0000 0000 0011 0000 0000 0000 0000
上面的一个32位的int表示有3个线程获取了读锁,0个线程获取了写锁
读/写锁如何确定和改变状态? ——>位运算
假设当前同步状态为state
//读状态:无符号右移16位
state >>> 16
//写状态:高16位都和0按位与运算,抹去高16位
state & Ox0000FFFF
//读状态加1
state + (1 << 16)
//写状态加1
state + 1
//判断写状态大于0,也就是写锁是已经获取
state & Ox0000FFFF > 0
//判断读状态大于0,也就是读锁是已经获取
state != 0 && (state & Ox0000FFFF == 0)
源码分析
AQS的内部类实现中的重点部分:
//定义一个偏移量
static final int SHARED_SHIFT = 16;
//1个读锁读锁单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT); //Ox00010000
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; //OxFFFF0000读锁上限
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //OxFFFF0000用于抹去高16位
//返回读状态
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//返回写状态
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
写锁的获取
protected final boolean tryAcquire(int acquires) {
//获取当前线程
Thread current = Thread.currentThread();
//获取当前同步状态
int c = getState();
//获取读状态
int w = exclusiveCount(c);
//如果同步状态不为0,说明有线程已经获取到了同步状态
if (c != 0) {
//读状态等于0(表示有线程已经获取读锁)或者当前线程不是已经获取写锁的线程
//返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//大于最大线程数则抛出错误
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//重入式的设置同步状态(写状态直接加)
//返回true
setState(c + acquires);
return true;
}
//如果同步状态等于0
//在尝试获取同步状态之前先调用writerShouldBlock()是根据公平锁还是非公平锁来判断是否应该直接阻塞(也就是是否能够直接获取)
//获取成功则设置为头节点并返回true
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
与ReentrantLock不同的是这里在写锁获取锁之前先判断是否有读锁存在,只有在读锁不存在的情况下才能去获取写锁(为了保证写的操作对所有的读都可见)。
写锁的释放
写锁的释放基本和ReentrantLock一致,只是获取写状态的地方稍有不同,获取的是低16位的值
protected final boolean tryRelease(int releases) {
//当前线程不是获取了同步状态的线程则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
//判断写状态是否为0
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
读锁的获取
读锁相对比较复杂,因为读锁还有类似于获取当前线程获得的锁的个数等方法。
//用于记录每个线程获取到的锁的数量
//使用id和count记录
static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}
//这里使用了ThreadLocal为每个线程都单独维护了一个HoldCounter来记录获取的锁的数量
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
//获取到的读锁的数量
private transient ThreadLocalHoldCounter readHolds;
//最后一次成功获取到读锁的线程的HoldCounter对象
private transient HoldCounter cachedHoldCounter;
//第一个获取到读锁的线程
private transient Thread firstReader = null;
//第一个获取到读锁的线程拥有的读锁数量
private transient int firstReaderHoldCount;
读锁的获取:
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);
//根据是否是公平锁来判断是否需要进入阻塞
//CAS设置同步状态
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果是第一个获取读状态的线程
if (r == 0) {
//设置firstReader和firstReaderHoldCount
firstReader = current;
firstReaderHoldCount = 1;
//如果当前线程和第一个获取读锁的线程是同一个线程那么它的获取的读锁数量加1
} else if (firstReader == current) {
firstReaderHoldCount++;
//是别的线程
} else {
//获取最后一次获取到读状态的线程
HoldCounter rh = cachedHoldCounter;
//rh == null(当前线程是第二个获取的),或者当前线程和rh不是同一个,那么获取到当前线程的HoldCounter
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
//如果rh就是当前线程的HoldCounter并且当前线程获取到的读状态位0那么给当前线程的HoldCounter设置为rh
else if (rh.count == 0)
readHolds.set(rh);
//获取到的读锁数加1
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
//自旋
for (;;) {
int c = getState();
//如果已经有写锁被获取
if (exclusiveCount(c) != 0) {
//如果获取写锁的线程不是当前线程则获取失败
if (getExclusiveOwnerThread() != current)
return -1;
//如果获取写锁的线程是当前线程则继续保持这个写锁
//如果此时应该进入阻塞
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
//第一次循环
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
//如果当前线程的读锁为0就remove,因为后面会set
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设置同步状态
//后续操作和tryAquireShared基本一致
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
读锁的获取比较繁琐,但是总的来说还是通过CAS设置同步状态,同时同步状态的设置不是淡出的加1二十加的1<<16。
同时这里的获取分为了tryAcquireShared和fullTryAcquireShared两步,个人认为第一次的tryAcquireShared是类似于快速尝试获取,而fullTryAcquireShared则是通过CAS+自旋的方式获取。如果第一次成功了就不用执行后面的循环了。
读锁的释放
读锁的释放和读锁的获取一样比价复杂:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果当前线程是第一个获取读锁的线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
//如果第一个获取读锁的线程只获取了一个锁那么firstReader=null
//否则firstReaderHoldCount--
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
//如果当前线程不是第一个获取读锁的线程
HoldCounter rh = cachedHoldCounter;
//获取当前线程的HoldCounter
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
//当前线程获取的读锁小于等于1那么就将remove当前线程的HoldCounter
readHolds.remove();
//当前线程获取的读锁小于等于0抛出异常
if (count <= 0)
throw unmatchedUnlockException();
}
//当前线程拥有的读锁数量减1
--rh.count;
}
//自旋
for (;;) {
int c = getState();
//释放后的同步状态
int nextc = c - SHARED_UNIT;
//CAS设置同步状态,成功则返回是否同步状态为0
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
锁降级
锁降级指的是先获取到写锁,然后获取到读锁,然后释放了写锁的过程。
因为在获取读锁的时候的判断条件是:
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
所以当前线程是可以在获取了写锁的情况下再去获取读锁的。
那么在写锁释放了之后应该还能继续持有读锁。
例:
下面的例子有两个写的线程,第一个写线程在写完之后还要进行读的操作,并且在读的时候sleep一秒,如果锁降级正常的话,在这1秒内另外一个写线程将不能获取到写锁
public class ReentratReadWriteLockDemo {
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock w = lock.writeLock();
private static Lock r = lock.readLock();
public static void main(String[] args) {
News news = new News();
//write and get
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 5; i++){
w.lock();
news.add(i + "");
r.lock();
w.unlock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String s = news.getLast();
System.out.println(Thread.currentThread().getName() + " get the last news" + s);
r.unlock();
}
}
}, "WriteAndGetThread").start();
//write
Thread t = new Thread(new Runnable() {
@Override
public void run() {
Thread.yield();
for(int i = 5; i < 10; i++){
w.lock();
news.add(i + "");
w.unlock();
}
}
}, "WriteThread");
t.start();
}
static class News {
private final List<String> newsList = new ArrayList<>();
public String getLast(){
if(newsList.size() == 0)
return null;
return newsList.get(newsList.size() - 1);
}
public void add(String news) {
newsList.add(news);
System.out.println(Thread.currentThread().getName() + " add :" + news);
}
}
}
输出:
WriteAndGetThread add :0
WriteAndGetThread get the last news0
WriteThread add :5
WriteThread add :6
WriteThread add :7
WriteThread add :8
WriteThread add :9
WriteAndGetThread add :1
WriteAndGetThread get the last news1
WriteAndGetThread add :2
WriteAndGetThread get the last news2
WriteAndGetThread add :3
WriteAndGetThread get the last news3
WriteAndGetThread add :4
WriteAndGetThread get the last news4
可以看到不存在这样的情况。也就是说锁是如我们预期一样降级的。
WriteAndGetThread add :m
WriteThread add :n
WriteAndGetThread get the last newsm
最后:锁是不支持锁升级的(先获取写锁,再获取读锁然后释放读锁),
因为第一步获取读锁的时候可能有多个线程获取了读锁,这样如果锁升级的话将会导致写操作对其他已经获取了读锁的线程不可见。
小结
ReentrantReadWriteLock(读写锁)允许同时多个线程获取读锁,但是只允许单个线程获取写锁。这使得在读的操作很多而写的操作很少的情况下非常的好用,但是当写的操作很多的的情况下就可能频繁的发生写锁的竞争而导致线程阻塞。同时由于一个32的int被分割使用导致读锁写锁的最大线程数量也有了限制(一般也没那么多线程。。)
并且读多写少的时候因为读会阻塞写,所以可能造成写线程“饥饿”的现象