​ Java中加入了可重入锁的具体实现:ReentrantLock,还有读写锁的实现:ReentrantReadWriteLock。

ReentrantLock的底层实现

​ ReentrantLock是维护了一个内部类Sync来实现的,这个内部类Sync继承了AQS,实现了不公平的加锁和解锁。并且实现了NonfairSync和FairSync来实现可重入锁的公平性和非公平性。

ReentrantLock实现可重入获取锁和解锁

​ 在ReentrantLock中的Sync中的加锁的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

​ 在方法nonfairTryAcquire中,先判断当前的共享资源有没有线程已经加锁,如果没有,使用CAS加锁,返回true,如果已经获得了锁,判断获得锁的线程是不是当前线程,如果是,同步状态+1,否则返回错误。如果在加锁过程中采用了超时加锁,那么具体实现是AQS内的实现方法。
ReentrantLock实现解锁的方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

​ 如果该锁被获取了n次,那么前n-1次的释放锁会返回false,只有同步状态完全释放了,才能够返回true。该方法把0作为释放条件,同步状态为0的时候才将占有线程设置为null,返回true,表示释放成功。

ReentrantLock实现公平性加锁和非公平性加锁

​ 在ReentrantLock的实现中,默认的加锁方法就是非公平性的加锁,也就是说,虽然实现了内部类NonfairSync继承了Sync,但是加锁的方法只是调用了Sync中的加锁方法:

1
2
3
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

​ 在公平锁的加锁方法里,重新进行了方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

​ 公平锁是按照申请顺序来进行加锁的,也就是说是FIFO顺序,所以在方法里面会先判断有没有前驱节点,如果没有前驱节点,那么这个节点就是头节点,可以进行加锁操作,这也就是通过实现AQS来搭建的好处,可以非常清楚的判断当前队列信息。
公平锁的解锁方式跟非公平锁并无区别。设定一个ReentrantLock是公平锁的实现还是非公平锁的实现,可以通过在构造函数中传入boolean变量来决定。

ReentrantLock中的公平锁和非公平锁对比

​ 在公平锁中,每一个线程都会得到锁,也就是都能得到运行机会,而在非公平锁中,如果一个线程在使用CAS加锁前恰好有一个其它线程得到了锁,那么这个线程将会加锁失败,而如果足够凑巧,那么非公平锁将会存在饥饿现象。
但是非公平锁与公平锁相比,非公平锁在执行过程中线程切换较少,所以系统的开销更小,保证了更大的吞吐量。

ReentrantReadWriteLock的底层实现

​ ReentrantLock或者synchronized方法都属于独占锁,也就是在同一时刻只能有一个线程持有该锁,在ReentrantReadWriteLock中实现了读写锁,也就是在读状态下,允许多个线程同时获取一个锁。
读写锁改进了在大量数据并发访问少量数据写入情况下的并发性能。
同ReentrantLock一样,在读写锁中通过维护一个私有的Sync的类来实现底层方法的构建。通过实现一个ReadLock和一个WriteLock来实现方法的调用。

ReentrantReadWriteLock实现读写状态

​ 在底层的内部类Sync的实现里,有这么几个变量和方法:

1
2
3
4
5
6
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;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

​ 也就是在ReentrantReadWriteLock中,同步状态是在一个整型变量上通过使用“按位切割使用”来进行读写锁的区分的。在一个32位的变量上,高16位表示的是读状态,低16位表示的是写状态。
读写锁获取读或者写的状态是通过位运算来进行的。假设同步状态为c,c>>>SHARED_SHIFT表示的是读状态(无符号位右移16位),c&EXCLUSIVE_MASK表示的是写状态(抹去高16位)。
所以,在c不等于0时,当写状态等于0 ,读状态大于0,也就是读锁已经被获取。

ReentrantReadWriteLock中读锁和写锁对于获取释放的实现

​ 写锁的获取等同于互斥锁的实现,也就是存在任何获取到锁的线程,这次获取就失败了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
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
29
@ReservedStackAccess
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);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}

​ 在Java6以后,获取读锁的方法中添加了当前线程获取锁的次数,所以实现复杂了许多。

锁降级

​ 锁降级是一种线程安全的实现方式,是写锁降级成为读锁。降级锁是在拥有写锁的时候,持有写锁,先获取读锁,然后释放写锁的过程。
这个过程中保证了数据的可见性,如果线程A不获取读锁而是直接释放写锁,如果此时有另外一个线程B获取写锁更改数据,那么线程A无法获取线程B中的数据更新,所以需要先持有读锁,然后阻塞另外线程B,知道A释放读锁之后,B才能够获取写锁进行数据更新。

写在文章后:前两次次接触位运算的概念也是在JDK的实现中,一次是在LinkedList扩容的时候,通过位运算确定扩容的大小;另一次是JDK中对于折半查找的实现的时候,通过位运算确定中间量的位置大小。然后这里通过位运算确定读和写的状态,忽然发现JDK实现里面可真是喜欢使用位运算。Java怕不是基于位运算编程...