前两天打蓝桥杯去了(呜呜呜,真的是水杯,顶不住了,能不能混个参与奖啊),没更新JUC。
读写锁来源
今天继续,我们今天来看另外一种锁,读写锁,顾名思义,他有同时有两个锁分别是读锁和写锁。那么有什么用呢?可以在读多写少的情况下提升系统整体性能,而在没有读写锁之前,我们只能靠wait/notify机制来进行线程的合作,写操作完成后notifyAll所有的读线程,在没有写完之前所有的读都是阻塞住的,写锁使用synchronized保证独占。而读写锁出来后,让程序编码有了更加简明的方式,以及一些新的功能,比如说锁降级...
读写锁说明
多个线程可以同时获取读锁,而如果一个线程获取了写锁,其他的读和写请求只能阻塞。,而当在写锁之前已经有读锁被获得,那么写锁会一直阻塞(因为他得保证数据的可见性),直到读锁释放,他才能获取写锁。
特性如下:
| 特性 | |
|---|---|
| 公平性 | 非公平锁性能优于公平锁 |
| 重入性 | 读锁和写锁都支持重入 |
| 锁降级 | 写锁能够降级为读锁,遵循获取写锁,获取读锁再释放写锁的次序 |
锁降级
公平性和重入性,毋庸置疑,我们已经了解。但是锁降级是什么东西?之前我们讲过获取了写锁,其余线程的读写锁请求都会被阻塞,但是我获取写锁自己是可以降级为读锁且不阻塞的。看看实例代码深入了解这句话
public static void main(String[] args) {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();//内置读锁
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//内置写锁
Thread t1 = new Thread("t1") {
@Override
public void run() {
writeLock.lock();//获取写锁
System.out.println("t1我获得了写锁");
readLock.lock();//锁降级
System.out.println("t1我获得了读锁,锁降级");
try {
sleep(1000);
System.out.println("t1我要放写锁了");
writeLock.unlock();
sleep(1000);
System.out.println("t1我要放读锁了");
readLock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
try {
//保证t1先执行
Thread.sleep(10);
System.out.println("t2我醒了,想得到写锁");
writeLock.lock();
System.out.println("t2我得到了写锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
t2.start();
}
执行结果如下
t1我获得了写锁
t1我获得了读锁,锁降级
t2我醒了,想得到写锁
t1我要放写锁了
t1我要放读锁了
t2我得到了写锁
从代码中看到,两个线程,保证让某个线程(这里是t1)先获取到了写锁,那么另一个线程t2就获取不了读写锁了,他进入阻塞,然后我t1在获取写锁的基础上,又获取了读锁,我们发现是可以直接获取的,并没有阻塞,这时候他就由写锁降级为了读锁,然后释放写锁读锁后t2就能拿到写锁了(获取写锁要保证已经没有其他线程有锁)。使用次序如上代码所示,写锁->读锁->释放写锁->释放读锁。
具体实现
看懂了上述事例,读写锁就使用的差不多了。那么我们看到,读写锁很灵活,编程方式也并不难。那么他是怎么实现的呢?下面我们就来研究一下读写锁的秘密。
读写设计
首先他也是基于AQS来实现的,但是我们发现AQS里面只有一个state变量,可以进行维护一把锁,那么你读写锁有两把锁是如何基于我AQS来实现的呢?答案是按位切分使用,一个state变量是int型,32位。然后我高16位读锁使用,低16位写锁使用。
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;//共享锁左移位数为16,也就是使用高16位作为读锁
static final int SHARED_UNIT = (1 << SHARED_SHIFT);//共享锁
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//独占锁标志,等价于0x0000FFFF
}
当我们使用读锁,假如当前同步状态位w,那么在高16位上+1(读锁),w+(1<<16);
我们使用写锁,获取读锁状态,w>>>16,能够获取到低16位。
AQS如何管理
我们看到读写锁,他是有两把锁,那么也就是共享和独占锁都会被AQS加入队列进行管理。那么如何加入呢,重写tryAcquire以及tryAcquireShared获取资源,以及对应的释放资源即可。
写锁的获取和释放
tryAcquire
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);//返回state中写锁的数量
if (c != 0) {//获取了读锁或者写锁的情况
//没有线程获取写锁(保证读锁全部被释放) 或 获取写锁的不是当前thread,进入队列阻塞
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//到这,一定是获取了写锁的,只有一个,所有不需要cas
setState(c + acquires);
return true;
}
//没有获取读,写锁的情况
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
写锁小节:在获取了写锁或者读锁的情况下,再有线程获取读写锁会返回false进入AQS阻塞。原因是为了保证可见性。没有获取读写锁,才会尝试去获取读锁。
tryRelease
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())//当前线程并没有获取写锁,抛出异常
throw new IllegalMonitorStateException();
int nextc = getState() - releases;//减去重入次数
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
这里比较简单,直接释放对应的重入量即可。
读锁的获取和释放
tryAcquireShared
这里由于为了getReadHoldCount(获取当前线程获取读锁次数,使用ThreadLocal进行存储)等方法使得tryAcquire变得复杂。但是主要逻辑如下(进行了删减)
final int fullTryAcquireShared(Thread current) {
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)//获取了写锁,且不是当前线程
return -1;
...
}
...
if (compareAndSetState(c, c + SHARED_UNIT)) {//c+SHARED_UNIT = c+(1<<16)即高位+1
return 1;
}
}
}
tryReleaseShared
同样这里也是进行了相应的删减
...
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;// c-(1<<16)
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
由于可能多个读锁同时释放,那么这里使用cas+自旋进行读锁的释放,保证原子性。
总结
引用开头的一句话“多个线程可以同时获取读锁,而如果一个线程获取了写锁,其他的读和写请求只能阻塞。,而当在写锁之前已经有读锁被获得,那么写锁会一直阻塞(因为他得保证数据的可见性),直到读锁释放,他才能获取写锁。”。
所有的代码都是实现这句话的。