读写锁

ReentrantReadWriteLock:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

读锁是共享锁,可以有多个线程读;而写锁是独占锁,同时只可能有一个线程写。

共享变量

读锁可以多线程访问,写锁只可以有一个线程访问,我们很容易想到可以使用两个变量来表示读写状态。但是,AQS却只是使用一个state来实现。

1
2
3
4
5
6
7
8
9
static final int SHARED_SHIFT   = 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;

/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

举个例子来看:

这里有两个关键方法sharedCount和exclusiveCount,通过名字可以看出sharedCount是共享锁的数量,exclusiveCount是独占锁的数量。

共享锁通过对c像右位移16位获得,独占锁通过和16位的1与运算获得。

state前十六位代表读锁,后十六位代表写锁。

举个例子,当获取读锁的线程有3个,写锁的线程有1个(当然这是不可能同时有的),state就表示为0000 0000 0000 0011 0000 0000 0000 0001,高16位代表读锁,通过向右位移16位(c >>> SHARED_SHIFT)得倒10进制的3,通过和0000 0000 0000 0000 1111 1111 1111 1111与运算(c & EXCLUSIVE_MASK),获得10进制的1。

由于16位最大全1表示为65535,所以读锁和写锁最多可以获取65535个

WriteLock

写锁是一把独占锁,同时只可能有一个线程访问,而且不可能与读锁同时存在,所以与ReentrantLock不同的是,WriteLock不仅要判断是否还有其它写线程占用,还要考虑是否还有读线程占用

读锁是否存在。因为要确保写锁的操作对读锁是可见的。如果在存在读锁的情况下允许获取写锁,那么那些已经获取读锁的其他线程可能就无法感知当前写线程的操作。因此只有等读锁完全释放后,写锁才能够被当前线程所获取,一旦写锁获取了,所有其他读、写线程均会被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected final boolean tryAcquire(int acquires) {

Thread current = Thread.currentThread();
int c = getState(); //获取共享变量state
int w = exclusiveCount(c); //获取写锁数量
if (c != 0) { //有读锁或者写锁
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) //写锁为0(证明有读锁),或者持有写锁的线程不为当前线程
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires); //当前线程持有写锁,为重入锁,+acquires即可
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires)) //CAS操作失败,多线程情况下被抢占,获取锁失败。CAS成功则获取锁成功
return false;
setExclusiveOwnerThread(current);
return true;
}

锁降级

在获取写锁的时候,如果资源存在读锁,因为可能存在多个不同的线程读,要是修改了线程除了本线程别的线程也感知不到,那么肯定是无法获取写锁的。

但是,在获取读锁的时候, 如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读锁(锁降级),主要原因是因为在同一个线程内,写锁所做的修改读锁时立即可见的,但是在别的线程内就没有可见性了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CachedData {
Object data;
//保证状态可见性
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 在获取写锁前必须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
//再次检查其他线程是否已经抢到
if (!cacheValid) {
//获取数据
data = ...
cacheValid = true;
}
// 在释放写锁之前通过获取读锁来降级
rwl.readLock().lock();
//释放写锁,保持读锁
rwl.writeLock().unlock();
}

use(data);
rwl.readLock().unlock();
}
}

ReadLock

  • 申请读锁,资源上没有写锁,且读锁数量小于最大值,申请读锁成功。
  • 申请读锁,资源上有写锁,且写锁就在本线程上,那么申请成功。

读写锁

锁降级

读写锁