java.util.concurrent包中的工具实现核心都是AQS,了解ReentrantLock的实现原理,需要先分析AQS以及AQS与ReentrantLock的关系。
同步控制中主要使用到的信息如上图所示。AQS可以被当做是一个同步监视器的实现,并且具有排队功能。当线程尝试获取AQS的锁时,如果AQS已经被别的线程获取锁,那么将会新建一个Node节点,并且加入到AQS的等待队列中,这个队列也由AQS本身自己维护。当锁被释放时,唤醒下一个节点尝试获取锁。
变量exclusiveOwnerThread在互斥模式下,表示当前持有锁的线程。
变量tail指向等待获取AQS的锁的节点队列的最后一个
变量head指向队列中head节点,head节点信息为空,不表示任何正在等待的线程。
-
变量state表示AQS同步器的状态,在不同情况下含义可能不太一样,例如以下几种
在ReentrantLock中,表示AQS的锁是否已经被占用获取,0:没有,>=1:
已被获取,当大于1时表示被同一线程多次重入锁。在CountDownLatch中,表示计数还剩的次数,当到达0时,唤醒等待线程。
在Semaphore中,表示AQS还可以被获取锁的次数,获取一次就减1,当到达0时,尝试获取的线程将会阻塞.
二者关联
ReentrantLock实现核心是基于AQS,看下面一张图,分析AQS与ReentrantLock的关系。
从图中可以看出,ReentrantLock里面有最终两个内部类,FairSync和NonfairSync通过继承AbstractQueuedSynchronizer的功能,来实现两种同步互斥方案:公平锁和非公平锁。在ReentrantLock中最终lock和unlock操作,都由FairSync和NonfairSync实际完成。
- NonfairSync分析
- 下面看一个最简单利用ReentrantLock实现互斥的例子。
ReentrantLock lock = new ReentrantLock();
//尝试获取锁
lock.lock();
//获得锁后执行逻辑......
//......
//解锁
lock.unlock();
- 在ReentrantLock的默认无参构造方法中,创建的是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
- NonfairSync#lock
下面分析lock.lock();这句代码是如何实现同步互斥的。
final void lock() {
if (compareAndSetState(0, 1))//【step1】
setExclusiveOwnerThread(Thread.currentThread());//【step2】
else
acquire(1);//【step3】
}
【step1】上面有提到,NonfairSync继承自AbstractQueuedSynchronizer,NonfairSync就是一个AQS,因此在步骤【1】其实就是利用CAS(一个原子性的比较并设置操作)尝试设置AQS的state为1。如果设置成功,表示获取锁成功;如果设置失败,表示state之前已经是>=1,已经被别的线程占用了AQS的锁,所示无法设置state为1,稍后会把线程加入到等待队列。
非公平锁与公平锁:对于NonfairSync非公平锁来说,线程只要执行lock请求,就会立马尝试获取锁,不会管AQS当前管理的等待队列中有没有正在等待的线程,这种操作是不公平的,没有先来后到;而稍后介绍的FairSync公平锁,则会在lock请求进行时,先判断AQS管理的等待队列中是否已经有正在等待的线程,有的话就是不尝试获取锁,直接进入等待队列,保证了公平性。
这一步需要熟悉的是CAS操作,分析一下compareAndSetState源码,如下。这一步利用unsafe包的cas操作,unsafe包类似一种java留给开发者的后门,可以用来直接操作内存数据,并且保证这个操作的原子性。在下面的代码中,stateOffset表示state比变量的内存偏移地址,用来寻找state变量内存位置。整个cas操作就是找到内存中当前的state变量值,并且与expect期待值比较,如果跟期待值相同,那么表示这个值是可以修改的,此时就会对state值进行更新;如果与期待值不一样,那么将不能进行更新操作。unsafe保证了比较与设置值的过程是原子性的,在这一步不会出现线程安全问题。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
【step2】操作是在设置state成功之后,表示当前线程获取AQS锁成功,需要设置AQS中的变量exclusiveOwnerThread为当前持有锁的线程,做保存记录。
【step3】当尝试获取锁失败的时候,就需要进行步骤3,执行acquire,进行再次尝试或者线程进入等待队列。
总结原理流程
当线程1调用lock方法时,首先看 AQS 的 state 是否为0,如果是0的话,通过cas操作将state置为1,并且设置独占线程为当前线程
如果这时候线程1 要调用另外一个lock方法,那么线程1会发现 state = 1,它再去看独占线程是不是就是自己,如果是的话 state + 1 ,获取锁成功。
如果线程1 执行的方法还没有完成即锁还没有释放,此时线程2调用lock方法,由于线程1没有释放锁,那么state不会等于0,且独占线程是线程1而不是自己(线程2),所以AQS会把线程2放到等待队列的尾部,如果线程2的前置结点是头结点head,那么线程2会通过死循环一直去获取锁,如果还是获取不到锁,那么会阻塞住线程2,下面的图有点问题啊。如果不是头结点那么就会阻塞线程2,等待线程1释放锁且唤醒它。