Renntrantlock原理(源码)
文章目录
前言
记录第一次写博客,在面试中,经常发现会问Synchronized与ReentrantLock的区别,所以深入理解下ReentrantLock实现的原理。我们知道Synchronized底层是JVM实现的,在字节码中修饰在方法层面的同步关键字,会多一个 ACC_SYNCHRONIZED(方法)的判别位,修饰在代码块层面的同步块会多一个 monitorenter和 monitorexit关键字,而Monitorenter和Monitorexit是监视器独有的,监视器包括3部分,owner(进入锁的占有线程)、Waitset(被wait()命令沉睡等待唤醒的线程)以及EntryList(阻塞队列)。底层Hostpot源码还没看,忙完这一阵子,抽时间研读一下。本文主要记录下自己认识的ReentrantLock的原理,便于日后学习。而他们两者的区别不多说了。(可中断,用拥有条件变量、可设置公平锁、可设置超时时间避免死锁)
一、ReentrankLock的AQS部分源码
首先我们知道,ReentrantLock是由AQS同步器框架而来的。首先看下AQS的部分源码。
下面展示一些 AQS获取锁的源码
。这种设计风格方式是模板设计模式。tryAcquire、tryRelease由AQS暴露给外部也可以理解为给ReentrantLock的AQS具体实现用的。
下面是Acquire方法,判别的是是否获取到锁,如果没有获取到锁,那么就在原先位置上加一个备份(包括该线程的状态),然后把本尊(线程本尊)放到阻塞队列(acquireQueued)。Release也类似。
子类只需要实现自己获取锁的逻辑和释放逻辑即可,至于排队阻塞等待、唤醒机制由AQS完成。
public final void acquire(int arg) {
if (!tryAcquire(arg) && //没有获取到锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //分身,并加入到阻塞队列中
selfInterrupt();
}
//子类实现获取锁的逻辑,AQS并不知道怎么用state上锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
public final boolean release(int arg) {
if (tryRelease(arg)) { //如果释放锁成功
Node h = head; //检查阻塞队列唤醒
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//子类获取锁的逻辑,AQS也不知道怎么释放锁(这些都给ReentranLock做)
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
二、Reentrantlock原理
在ReentrantLock使用中,通常用lock方法。以下是Lock方法的源码实现。
public void lock() {
sync.lock();
}
不难看出,lock方法调用了Sync的Lock方法,点进去看下。
这里可以清晰的表示Lock是抽象的方法,往下看实现的两个方法,分别为FairSync与NofairSync。说明lock方法是根据公平与非公平进行的。以下是公平锁与非公平锁的原理。
非公平锁比公平锁效率高(公平锁需要排队与唤醒时间也就是上下文切换 + 调度时间)
三.ReentrantLock源码实现
3.1核心变量和构造器
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
上图可以看出,默认创建的非公平锁,在构造器中可以控制使用公平锁或者是非公平锁。
3.2.公平与非公平锁(获取锁)实现原理
非公平代码流程:先在reentrantlock类中继承了抽象内部类,再调用Sync静态内部类中的 nonfairTryAcquire。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
//非公平锁直接抢锁,不管有没有线程排队(用CAS)尝试将state置为1
//如果置为1了,在OwnerThread中把当前线程加入在里面。(这种方案和monitor的owner类似)
if (compareAndSetState(0, 1))
//上锁成功,标识当前线程为获取锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
//抢锁失败,进入AQS的标准获取锁流程
//这里是AQS,如果下面tryrequire返回true,则又获取到锁了,否则进入阻塞队列。详细请看AQS源码解析。
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
//使用AQS提供的获取非公平锁方法获取锁
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
//调用非公平锁标准获取方法
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()) {
//利用state政协变量记录次数
int nextc = c + acquires;
//如果超过int表示范围,表名符号溢出,所以抛出异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//表名需要AQS来将当前线程放入阻塞队列,然后进行阻塞操作,等待唤醒
return false;
}
下面是公平锁的实现源码。
公平代码流程:先在Reentrantlock类中继承了抽象内部类,但是这里公平锁的tryacquire在本方法中实现,没有在Sync中。为什么呢?李大爷喜欢。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//有reentrantlock调用
final void lock() {
//没有尝试抢锁,直接进入AQS的标准流程。
acquire(1);
}
//AQS调用,子类自己实现获取锁的流程
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//此时正好获取锁的线程释放了锁,也有可能本身没有线程获取锁
if (c == 0) {
//这里和非公平锁区别在于:hasQueuedPredecessors看看队列中是否有线程正在排队,没有在通过CAS抢锁。
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;
}
//AQS来将当前线程放入阻塞队列
return false;
}
}
3.2.释放锁操作
公平锁与非公平锁公用方法,因为在释放锁的时候,并不区分是否公平。
public void unlock() {
sync.release(1);
}
protected final boolean tryRelease(int releases) {
//拿到锁的重入次数
int c = getState() - releases;
//如果当前线程不是上锁的线程,是无效的。
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//不是重入锁,那么当前线程一定是释放锁了,把当前AQS用于保存当前锁对象的变量ExclusiveOwnerThread设置为null,表明释放锁成功。
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//此时的state全局变量没有改变,也就是在setState之前,没有别的线程获取到锁,这保证了以上操作的原子性。
setState(c);
//告诉AQS,当前释放锁成功,可以唤醒正在等待锁的线程了。
return free;
}
总结
在上述,还有一些源码没涉及,但是如果深入研究,比如条件变量,这里是直接引用了AQS的条件变量,所以先研究AQS,也就水到渠成了。
public class test82 {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
try {
//互斥区
}finally {
reentrantLock.unlock();
}
}
}
在上述代码中,new的是非公平锁,调用lock对象时,由于此时并没有锁锁住它,调lock的时候回调Sync的lock方法,而lock方法里面对于非公平锁用的是CAS成功直接返回。而调用unlock时,获取当前的state-1,看是否为0,如果为0,则成功。
RenntrantReadwritelock这个同步器和reentrantlock类似,区分为锁分为读锁与写锁。写写互斥,写读互斥,读读不互斥。
RenntrantReadwritelock源码暂时看了些,主要看AQS源码,过几天总结下AQS源码实现。