文章目录
1. java主流的锁
在存在数据竞争的场景下,经常要用到锁。除了语言层面的synchronized关键字,java还在jdk里提供了功能丰富的锁相关api。
锁相关的概念有很多,synchronized关键字的特性可以想到 自旋,偏向锁,轻量级锁,重量级锁。ReentrantLock实现公平锁&非公平锁,ReentrantReadWriteLock还实现共享锁和独占锁。
java锁的概念如下图:
与synchronized的相关锁概念,如锁消除,锁粗化,自旋锁,偏向锁,轻量级锁,重量级锁,已在虚拟机的锁优化技术提及。
本文主要研究一下ReentrantLock、ReentrantReadWriteLock的源码,看看jdk如何实现各种功能的锁。
2. 公平锁 & 非公平锁
2.1 公平锁
概念
公平锁是指多个线程按照申请锁的顺序来获取锁,具体实现是线程在抢锁时,先判断当前线程是不是队列头的线程,如果不是添加到队列尾排队。如果是,才尝试获取锁。
优缺点
公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
2.2 非公平锁
概念
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
优缺点
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
2.3 源码
ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
下面我们来看一下公平锁与非公平锁的加锁方法的源码:
公平锁:
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors 判断当前线程是否位于同步队列中的第一个。
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;
}
非公平锁:
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
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;
}
可以看到在锁没有被占用时,公平锁和非公平锁的区别是先判断当前线程是不是队列头的线程。
综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。
3. 可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
源码
加锁代码nonfairTryAcquire()里的以下逻辑,就是可重入的实现
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
获取锁时先判断持有该锁的线程是不是当前线程,如果是,state加1,释放锁时state减1。
4. 独占锁 & 共享锁
4.1 概念
独占锁也叫排它锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是排它锁。
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
4.2 源码结构
jdk通过ReentrantReadWriteLock实现独占锁和排它锁,
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
/**
* 默认非公平
*/
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
}
ReentrantReadWriteLock内部定义两个锁,ReadLock和WriteLock,也就是共享读锁和独占写锁。实现ReadWriteLock的writeLock
,readLock
方法来获取读锁或写锁。两个锁是内部类,实现了Lock接口,具备加锁解锁的逻辑。
ReadLock:
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
}
WriteLock:
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);
}
}
ReentrantReadWriteLock内部还定义一个sync属性,它继承至AQS,实现共享和独占的逻辑。AQS是JUC一个核心类,它的子类在CountDownLatch、ReentrantLock、Semaphore里面也都存在。
4.3 优势
在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样,通过不同的方法实现。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。
4.4 源码实现
AQS里定义了一个32位的int字段 state,来记录锁的状态,
在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。
4.4.1 写锁加锁源码
写锁加锁的源码:
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
// 取到当前锁的个数
int c = getState();
// 取到当前写锁的个数
int w = exclusiveCount(c);
// 如果已经有线程持有了锁(c!=0)
if (c != 0) {
// 如果写线程数(w)为0(说明存在读锁) 或者持有锁的线程不是当前线程就返回失败(加了读锁不能再加写锁)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,加写锁成功
setExclusiveOwnerThread(current);
return true;
}
以上代码体现了加写锁的一个原则:存在读锁则不能加写锁。
4.4.2 读锁加锁源码
读锁加锁源码:
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
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 != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。
读锁加锁过程相比ReentrantLock,少了判断是否是当前线程的逻辑,ReentrantLock是独占锁。
5. 总结
jdk已经实现了功能丰富的锁的api,了解这些锁的概念和原理,在实际运用中选择合适的锁,可以很好地优化我们程序的性能。源码中的一些设计思路,也值得我们学习和借鉴。