本篇文章主要介绍下Java并发包内的ReentrantReadWriteLock工具类的读锁。首先它具有可重入锁的功能(可重入锁的介绍可以参考JAVA并发之ReentrantLock原理解析),同时也实现了ReadWriteLock接口,具有读锁(共享锁)和写锁(排他锁)两把锁。
读锁(共享锁)
读锁是共享锁,也分公平锁和非公平锁,在没有写锁在执行或者在AQS队列排队的时候,两者没有区别,会直接将读锁的count加1(线程退出读锁的时候调用unlock会让state减1)。
这里需要注意的是读锁和写锁的count是共用了AQS的state变量的,他是一个32位的int型变量,高16位表示读锁的count,低16位表示写锁的count,再理解下面的源码就能明白了。
//AQS
static final int SHARED_SHIFT = 16;
//获取读锁的count,将state右移16位,也就是取高16位
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//下面是tryAcquireShared方法当中判断读锁能否获取到锁的部分逻辑
//其中compareAndSetState(c, c + SHARED_UNIT)表示将state加上1左移16位,反应到sharedCount方法就是相当于加了1
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
...
}
下图是读锁调用的整个过程,当只有读锁的时候,方法走到第四步fullTryAcquireShared就返回了,这种情况下肯定能获得读锁。
下面我们来看下每一步大致的逻辑。
其中主要的方法有两个:
- tryAcquireShared:
- 如果有其他线程拿有写锁,当前线程就调用doAcquireShared进AQS队列等待;
- 如果拿有写锁的线程是当前线程,当前线程继续尝试获取读锁;
- 当前线程尝试拿读锁的时候需要判断是否要排队,这里公平锁和非公平锁表现就有差异了:
- 公平锁:如果AQS当中有线程在排队,则当前线程也需要排队
- 非公平锁:如果AQS当中有线程排队且不是读线程时,当前线程需要排队
- 如果当前线程尝试获取锁成功了,则直接返回,否则进入fullTryAcquireShared接着尝试
- doAcquireShared
- 将当前线程加到AQS的尾部
- 再次尝试一下tryAcquireShared(万一其他线程释放锁了呢)
- 如果成功则拿到锁,唤醒下一个在AQS中等待的读锁线程,返回
- 如果不成功,则标记AQS中前节点,告诉它等你释放锁的时间记得唤醒我,然后休眠等待被唤醒
读锁举例
下面通过例子来看下读锁的几种情况:
- 当前只有读锁
结论:最简单的情况,所有线程都能获得读锁。
- 有线程拿了写锁,并且不是当前线程
结论:当前线程进队列等待,等写锁释放了才有可能获取读锁
下面通过代码来验证下我们的结论。
/**
* 测试当其他线程获取写锁时,当前线程不能同时获取读锁
*
* @throws Exception
*/
private static void testReadLockWhenAnotherThreadHasWriteLock() throws Exception {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
DemoThread readThread = new DemoThread("ReadThread", readLock);
DemoThread writeThread = new DemoThread("WriteThread", writeLock);
writeThread.start();
Thread.sleep(1000);
readThread.start();
}
static class DemoThread extends Thread {
private DateFormat df = new SimpleDateFormat("HH:mm:ss---");
private Lock lock;
public DemoThread(String name, Lock lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
lock.lock();
System.out.println(df.format(new Date()) + getName() + " is working.");
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
System.out.println(df.format(new Date()) + getName() + " finished work.");
lock.unlock();
}
}
}
以上代码,我们创建了两个线程,写线程WriteThread先获取了写锁,5秒钟后释放,然后另一个读线程ReadThread需要等到写锁释放才能获取读锁。
12:56:53---WriteThread is working.
12:56:58---WriteThread finished work.
12:56:58---ReadThread is working.
12:57:03---ReadThread finished work.
- 当前线程拿了写锁,再去获取读锁
结论1: 当AQS没有其他线程排队时,当前线程能立刻获取读锁
同样我们看下代码
/**
* 测试当其他线程获取写锁时,当前线程不能同时获取读锁
*
* @throws Exception
*/
private static void testReadLockWhenSameThreadHasWriteLock() throws Exception {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try {
writeLock.lock();
System.out.println("I got the write lock.");
readLock.lock();
System.out.println("I got the read lock.");
} finally {
readLock.unlock();
writeLock.unlock();
}
}
控制台打出来的日志显示,当前线程获取写锁后,也获取到了读锁。
I got the write lock.
I got the read lock.
结论2: 当AQS有其他线程排队获取写锁时,当前线程需要排队等待
/**
* 测试当前线程获取写锁后,如果AQS有其他线程要获取写锁,需要排队获取读锁
*
* @throws Exception
*/
private static void testReadLockWhenSameThreadHasWriteLockAndAQSHasWriteThread() throws Exception {
DateFormat df = new SimpleDateFormat("HH:mm:ss---");
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
DemoThread2 readWriteThread = new DemoThread2("ReadWriteThread", lock);
readWriteThread.start();
Thread.sleep(1000);
System.out.println(df.format(new Date()) +"MainThread start to gain WriteLock");
writeLock.lock();
System.out.println(df.format(new Date()) +"MainThread got WriteLock");
Thread.sleep(5000);
writeLock.unlock();
System.out.println(df.format(new Date()) +"MainThread released WriteLock");
}
static class DemoThread2 extends Thread {
private DateFormat df = new SimpleDateFormat("HH:mm:ss---");
private ReentrantReadWriteLock lock;
public DemoThread2(String name, ReentrantReadWriteLock lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
writeLock.lock();
System.out.println(df.format(new Date()) + getName() + " got WriteLock.");
Thread.sleep(5000);
writeLock.unlock();
System.out.println(df.format(new Date()) + getName() + " release WriteLock.");
System.out.println(df.format(new Date()) + getName() + " try to get ReadLock.");
readLock.lock();
System.out.println(df.format(new Date()) + getName() + " got ReadLock.");
readLock.unlock();
System.out.println(df.format(new Date()) + getName() + " released ReadLock.");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
控制台显示当线程ReadWriteThread获取写锁后,主线程MainThread尝试获取写锁失败进入AQS排队,然后ReadWriteThread尝试获取读锁也失败进入了AQS排队。
13:28:49---ReadWriteThread got WriteLock.
13:28:50---MainThread start to gain WriteLock
13:28:54---ReadWriteThread release WriteLock.
13:28:54---MainThread got WriteLock
13:28:54---ReadWriteThread try to get ReadLock.
13:28:59---MainThread released WriteLock
13:28:59---ReadWriteThread got ReadLock.
13:28:59---ReadWriteThread released ReadLock.
结论3: 当读写锁是公平锁时,AQS若有其他线程排队获取读锁时,当前线程需要排队等待
结论4: 当读写锁是非公平锁时,AQS若有其他线程排队获取读锁时,当前线程不需要排队可立刻获取读锁
结论3和结论4由于篇幅原因这里就不贴代码了,感兴趣的朋友可以参考下面的gitee代码。
Demo代码位置
src/main/java/net/weichitech/juc/ReentrantReadWriteLockTest.java · 小西学编程/java-learning - Gitee.com