一、简介
在进行多线程编程的时候,考虑的最多的问题之一就是并发的问题,也就是多线程对于共享变量的操作问题。许多时候我们会借助于锁这个工具来控制多线程并发对于共享变量的操作,是一种牺牲时间的并发保障机制。
在java中关于同步工具,有自带的关键字synchronized,也有juc下利用AQS同步框架封装的Lock工具,以及一些以AQS及其Lock为基础的同步工具CountDownLatch、CycleBarrier等,关于AQS的理解,可以在《JUC-AQS框架解析》文章中进行源码级别的理解分析,本文章是在了解AQS基础之上,来分析Lock工具的源码逻辑执行过程。
二、概况
juc(java.util.concurrent)下locks包下封装了我们经常使用的并发工具Lock,上面文章介绍的AQS也是在此包目录下,在Lock以及ReadWriteLock接口下分别实现了普通的锁和按照读写功能实现的读写锁,首先我们看下Lock系列工具的关系:
可以看到Lock和ReadWriteLock分别是两个顶级的接口,ReentrantLock和ReentrantReadWriteLock分别是两个接口的实现类,前者是我们在并发中经常使用的普通可重入锁,后者则是读写锁,意在提供读读不冲突的锁类型,满足不同的操作锁。
三、解析
首先我们看下Lock接口定义信息,源码如下:
public interface Lock {
//获取锁
void lock();
//响应中断的获取锁
void lockInterruptibly() throws InterruptedException;
//尝试加锁,返回是否加锁成功
boolean tryLock();
//一段时间内尝试加锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//锁条件
Condition newCondition();
}
可以看到Lock接口大致定义了获取锁和释放锁的几个方法,下面我们看下ReentrantLock的具体逻辑,首先我们从ReentrantLock的加锁方法进入,也就是lock()方法,源码如下:
public void lock() {
sync.lock();
}
直接掉用了内部类Sync的lock()方法,我们看下Sync的lock()方法,源码如下:
abstract void lock();
Sync作为ReentrantLock的内部类,继承了AQS,也就是前面文章提到的AbstractQueuedSynchronizer框架,并且lock()作为抽象方法,他的子类都要去重写,因为ReentrantLock支持公平和非公平锁,依据传入的参数决定。Sync分别有两个子类,实现了fair和unfair锁方式,默认的方式是非公平锁,因此我们首先看下非公平锁的sync子类加锁方式,源码如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//加锁
final void lock() {
//加锁成功,设置当前线程为独占线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//加锁失败
acquire(1);
}
//重写AQS的tryAcquire()方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
可以到过程比较简单,首先cas去获取锁,如果成功,则设置当前线程为独占锁线程;如果cas失败则调用acquire()方法,此方式是我们在上篇《JUC-AQS框架解析》文章中讲解到的,他是独占锁的顶层入口,不理解的可以在去查看下,顶层入口获取锁的方法逻辑会调用自定义同步器也就是ReentrantLock的内部类Sync及其他的两个子类重写的获取锁方法,也是上面代码片段的tryAcquire()方法,我们可以看到他调用的是父类Sync的nonfairTryAcquire()方法,下面我们看下父类Sync的nonfairTryAcquire()方法源码结构:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//未加锁,则cas获取锁
if (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;
}
首先判断共享资源是否是空闲状态,如果是则cas去获取锁,成功则设置占用锁的线程,如果失败则返回false;如果资源被占用,则判断是否是本线程占用,也就是可重入锁的概念,如果是则累加共享变量,如果不是则返回false。
当获取锁失败的时候,则就进入了AQS的后续入队等待操作接口,顶层AQS的acquire()方法逻辑如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
以上是ReentrantLock非公平锁方式获取锁的逻辑,下面我们看下锁释放锁的逻辑:
public void unlock() {
sync.release(1);
}
可以看到掉用了Sync的父类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()方法,在ReentrantLock的Sync父类中:
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);//锁释放,占用线程为null
}
setState(c);
return free;
}
可以看到释放锁的过程也是比较简单的,首先校验一下当前线程是否是占有锁的状态,不是则直接抛出异常,如果释放资源后,是无锁状态(state=0)则设置占用锁的线程标识为null,然后设置state,返回free。state为0,则free为true,则唤醒后续线程去获取共享资源,如果state不是0,则free为false,则证明本线程重入锁,未释放完毕,不唤醒后续节点线程。
下面我们看下公平锁模式下获取锁的过程,首先我们看下公平锁的sync子类的lock()逻辑:
final void lock() {
acquire(1);
}
可以看到,和非公平锁逻辑不同之处在于,非公平锁是先cas获取锁,看能否成功,而公平锁则是不可以直接获取锁,而是直接入队等待获取锁,下面我们看下公平锁重写的tryAcquire()方法:
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;
}
可以看到比非公平锁多了一个hasQueuedPredecessors()方法的逻辑,判断是否有前节点的方法,逻辑如下:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
返回false,也就是可以尝试去获取锁的情况有:
1.h==t:头尾节点相同,可以都是null,或者目前队列只有一个节点;
2.h!=t,h.next() !=null,则看当前线程是不是头节点的下一个节点线程,是则返回false;
回过头来,看下公平锁的加锁过程,首先直接执行入队的获取锁方法,只有在最前面的节点线程菜有资额获取锁。公平和非公平的含义在此实现,新的线程到来如果共享资源可用,则公平锁还要判断本线程是否是最前面的节点线程,否则无资格获取锁。
以上就是lock公平锁以及非公平锁的获取、释放锁逻辑,ReentrantLock内部只是定义了获取和释放锁的逻辑,体现在tryAcquire()和tryRelease()方法的逻辑,上层的等待入队操作等,则是AQS内部已经实现好的功能。
剩下的一些方法包括响应中断的获取锁的方法、直接返回是否获取锁的方法以及尝试传入时间内获取锁的方法,逻辑都很容易理解,可以自行去查看理解。
四、资源地址
文档:《Thinking in java》
jdk1.8版本源码