文章目录
- 1. ReentrantLock概述
- 2. 非公平锁
- 2.1 加锁过程
- 2.2 解锁过程
- 3. 公平锁
- 3.1 加锁过程
- 3.2 解锁过程
- 4. 总结
- 5. 自定义锁
1. ReentrantLock概述
ReentrantLock意思为可重入锁,也就是能够多重加锁。并且加了多少次锁,也必须对应解锁多少次。
此外,ReentrantLock支持公平锁和非公平锁,是基于AQS进行实现的。关于公平锁和非公平锁可以看:【基本功】不可不说的Java“锁”事
- 公平锁:按照AQS的FIFO的同步队列的时间顺序获取锁,不会存在饥饿现象。
- 非公平锁:有可能刚刚释放锁的线程又重新获得锁,可能会出现线程饥饿现象。
下面我们来看下ReetrantLock和synchronizer之间的对比,如下图:
AQS中实现了独占锁和共享锁的框架。而ReentrantLock也是基于AQS进行实现的。只支持独占锁。
具体来说, ReentrantLock定义了静态内部内Sync,Sync类继承自AbstractQueuedSynchronizer。又因为ReentrantLock包括公平锁和非公平锁两种实现,所以又分别定义了FairSync和NonfairSync类,继承自Sync类,代表公平锁和非公平锁的各自实现。
从ReentrantLock的构造函数来看,它在默认情况下是创建非公平锁的。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
因为非公平锁相比于公平锁,可能在某些情况下可以减少上下文的切换,节省资源的消耗。
2. 非公平锁
2.1 加锁过程
我们首先进入非公平锁的lock()函数中:
final void lock() {
if (compareAndSetState(0, 1)) // CAS尝试获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 进入AQS的代码中尝试获取锁
}
从上面的代码中可以看到,一上来就尝试用CAS去修改state,也就是尝试获取锁(并没有排队),因为state就是AQS用来同步状态的。如果获取成功,那就插队成功了.
如果获取失败,就会进入AQS中的acquire函数继续尝试获取:
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
由我们之前讲过的AQS原理可知,AQS中的tryAcquire方法是空方法,需要自定义的同步器进行实现。所以我们这里就会调用ReentrantLock中NonfairSync重写的tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); // 进一步调用
}
进一步调用nonfairTryAcquire方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread(); // 获取当前线程
int c = getState(); // 获取当前的state
if (c == 0) { // 如果没有线程获取锁
if (compareAndSetState(0, acquires)) { // 尝试CAS获取锁
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); // 更新state
return true;
}
return false; // 获取锁失败
}
从上面的方法中可以看到,如果state=0,那么就立马去尝试抢锁。如果发现本身持有锁的就是当前线程,那么就可以通过增加state进行重入。
如果通过tryAcquire方法抢锁失败,那么就只能老老实实地回到AQS的队列中进行排队了。
整个的流程图如下:
2.2 解锁过程
解锁过程我们首先会进入到unlock函数中:
public void unlock() {
sync.release(1);
}
接着我们会进入AQS的release函数:
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试解锁
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后面的进程
return true;
}
return false;
}
然后就会调用tryRelease函数尝试解锁,但是在AQS中tryRelease函数是空函数,需要自定义的同步器自己实现。所以我们就会进入到ReentrantLock的Sync类中的tryRelease函数中(公平锁和非公平锁的解锁过程是一样的)。
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 在state上减少重数
if (Thread.currentThread() != getExclusiveOwnerThread()) // 如果发现锁不是当前线程的,则抛出异常
throw new IllegalMonitorStateException();
boolean free = false; // 判断是否完全释放的标志位
if (c == 0) { // 如果加锁重数=解锁重数,那么就完全释放了
free = true; // 完全解锁
setExclusiveOwnerThread(null);
}
setState(c); // 更新state
return free; // 返回是否完全释放锁
}
解锁成功后,就继续回到AQS唤醒后续的进程。解锁结束。
3. 公平锁
3.1 加锁过程
首先是会进入到公平锁的lock函数中:
final void lock() {
acquire(1); // 尝试获取锁
}
可以看到,他这里是直接调用了AQS中的acquire函数:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
根据上面的非公平锁的介绍,可以知道tryAcquire会调用自定义的同步器中重写的方法。
因此我们进入到ReentrantLock中FairSync类的tryAcquire函数:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread(); // 获得当前线程
int c = getState(); // 获取state
if (c == 0) { // 如果当前没有人持有锁
if (!hasQueuedPredecessors() && // 队列是否有前驱
compareAndSetState(0, acquires)) { // CAS尝试持有锁
setExclusiveOwnerThread(current); // 设置当前线程独占
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果本身是当前线程持有错
int nextc = c + acquires; // 进行重入
if (nextc < 0) // 重数过多
throw new Error("Maximum lock count exceeded");
setState(nextc); // 更新state
return true;
}
return false; // 获取锁失败
}
}
可以看到,公平锁和非公平锁的tryAcquire,主要区别就是在公平锁中,只有队伍中没有前驱节点,才会去尝试CAS获得锁。而在非公平锁中,不管当前等待队伍中是否有节点,都会直接尝试用CAS获得锁。
如果在tryAcquire中没有获取到锁,就会重新进入到AQS的队列中进行排队了。
3.2 解锁过程
在ReentrantLock的公平锁和非公平锁中,解锁过程都是一样的,都是在ReentrantLock中,所以这里就不过多赘述。
4. 总结
ReentrantLock其实是基于AQS框架来实现的一个同步器。以非公平锁为例,我们通过一张图来看下ReentrantLock整个加锁和解锁的过程:
可以看到,ReentrantLock构建了对外的两个api lock和unlock,并且重写了AQS中需要实现的方法tryAcquire、tryRelease。而AQS就主要是提供一个框架的作用,完成了锁的底层实现,使得ReentrantLock不需要关心在底层具体要如何实现。具体如下图:
5. 自定义锁
首先,我们自定义一个类Mutex,用来对外提供锁的api接口。然后,我们在Mutex中创建静态内部类Sync(AQS推荐同步器使用静态内部类继承的方式实现),重写tryAcquire和tryRelease方法。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class Mutex {
private Sync sync = new Sync();
public void lock(){ // 对外的加锁api
sync.acquire(1);
}
public void unlock(){ // 对外的解锁api
sync.release(1);
}
// 自定义的同步器
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) { // 重写AQS中的tryAcquire方法
return compareAndSetState(0,arg);
}
@Override
protected boolean tryRelease(int arg) { // 重写AQS中的tryRelease方法
setState(0);
return true;
}
}
}
至此,我们就很轻松地自定义了一个锁。
接下来,我们写一个类进行测试。多线程对静态变量count进行+1操作。
public class TestCount {
public static Mutex mutex = new Mutex(); // 自定义的锁
public static int count = 0; // 共享变量count
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
public void run() {
try {
mutex.lock();
for(int i=0;i<10000;i++){
count++;
}
}finally {
mutex.unlock();
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
Thread.sleep(1000);
System.out.println(TestCount.count);
}
}
因为我们在自增的前后加入了锁进行同步,这就使得每次输出的结果都是20000。
参考文章:从ReentrantLock的实现看AQS的原理及应用