ReentrantLock 公平锁和非公平锁的实现原理

顾名思义,ReentrantLock就是可重入锁的意思,Java关键字Synchronized也是实现的可重入锁。
首先我们来看一下它们两之间的异同点。

ReentrantLock和Synchronized的异同点:

特性 Synchronized ReentrantLock 是否相同
可重入 是 是 √
响应中断 否 是 ×
超时等待 否 是 ×
公平锁 否 是 ×
非公平锁 是 是 √
是否可尝试加锁 否 是 ×
java内置特性 是 否 ×
自动释放锁 是 否 ×
对异常的处理 自动释放锁 不会自动释放锁 ×
有上表可知,ReentrantLock功能是如此强大,那么我们就看看其中公平锁和非公平锁是如何实现的。

一. Sync内部类
1.1 Sync类源码
abstract static class Sync extends AbstractQueuedSynchronizer,我们可以看到Sync内部类继承了AQS,关于AQS,可以参考JDK源码系列 AbstractQueuedSynchronizer源码剖析这篇文章,AQS是一个很重要的类,我们必须掌握其中的原理。
接下来我们来看一下Sync内部类的源码实现:

abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/加锁函数 由子类进行实现/
abstract void lock();
/非公平锁尝试获取资源的函数/
final boolean nonfairTryAcquire(int acquires) {
/获取当前的线程/
final Thread current = Thread.currentThread();
/获取资源的状态/
int c = getState();
/若为0 说明当前资源没有被任何线程占有/
if (c == 0) {
/通过CAS尝试获取资源/
if (compareAndSetState(0, acquires)) {
/若获取资源资源成功 设置当前线程为独占线程/
setExclusiveOwnerThread(current);
return true;
}
}
/*若当前线程已经获取了资源
这一步是实现锁的可重入
/
else if (current == getExclusiveOwnerThread()) {
/更新资源的状态值/
int nextc = c + acquires;
/
这里说明一下 由于是可重入锁,那么每次重入就记录一下重入的次数
最大的重入次数为Integer.MAX_VALUE
若状态值超过Integer.MAX_VALUE,直接抛出异常
*/
if (nextc < 0) // overflow
throw new Error(“Maximum lock count exceeded”);
/更新资源的状态值/
setState(nextc);
return true;
}
return false;
}

/*释放资源的函数*/
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    /*如果当前线程并非独占线程并且尝试释放资源 直接抛出异常*/
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //当重入次数变成0 说明锁即将释放
    if (c == 0) {
        free = true;
        /*将独占线程设置为null*/
        setExclusiveOwnerThread(null);
    }
    //更新状态
    setState(c);
    return free;
}
/*判断当前线程释放是独占线程*/
protected final boolean isHeldExclusively() {
    return getExclusiveOwnerThread() == Thread.currentThread();
}
/*返回一个等待队列*/
final ConditionObject newCondition() {
    return new ConditionObject();
}
/*获取独占线程*/
final Thread getOwner() {
    return getState() == 0 ? null : getExclusiveOwnerThread();
}
/*获取线程 重入锁的次数*/
final int getHoldCount() {
    return isHeldExclusively() ? getState() : 0;
}
/*判断当前线程释放加锁*/
final boolean isLocked() {
    return getState() != 0;
}
/*这里和序列化相关*/
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    /*获取序列化对象*/
    s.defaultReadObject();
    /*重置资源的状态*/
    setState(0); // reset to unlocked state
}

}
以上就是Sync内部类源码的分析,其实着重看nonfairTryAcquire和release这两个方法即可,其他基本的状态值基本都是AQS类维护的,是通过AQS的机制来实现锁的。
至于可重入锁的实现,其实只是多了current == getExclusiveOwnerThread()这步的处理,每重入一次,我们就对资源
状态值进行累加,最大重入次数为Integer.MAX_VALUE/acquires次。release函数释放资源就是对状态值进行累减,直至为0才表示当前线程正式释放锁的资源。

接下来我们就来看一下非公平锁和公平锁的实现。

1.2. NofairSync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/加锁操作/
final void lock() {
/先通过cas去获取资源/
if (compareAndSetState(0, 1))
/获取到资源后设置独占线程为当前线程/
setExclusiveOwnerThread(Thread.currentThread());
else
/若cas获取不到资源 再通过acquire(1)进行资源的获取/
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
/重写tryAcquire方法,这个方法会被acquire(1)函数调用/
/通过nonfairTryAcquire(acquires)取尝试获取锁资源/
return nonfairTryAcquire(acquires);
}
}

1.3 FairSync
static final class FairSync extends ReentrantLock.Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
/与上面的不同 FairSync不会通过cas先取获取资源 而是直接进行acquire(1)/
acquire(1);
}
/尝试获取锁/
protected final boolean tryAcquire(int acquires) {
/取当前线程/
final Thread current = Thread.currentThread();
/获取资源的状态/
int c = getState();
/如果资源状态为0 说明当前没有任何线程占用资源/
if (c == 0) {
/如果Sync队列没有任何正在等待的线程 并且当前线程获取资源成功/
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() {
/获取尾结点/
AbstractQueuedSynchronizer.Node t = tail;
/获取头结点/
AbstractQueuedSynchronizer.Node h = head;
AbstractQueuedSynchronizer.Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

其实这个方法就是用于判断同步队列中有比当前线程等待时间更长的线程。
我们着重来看h != t && ((s = h.next) == null || s.thread != Thread.currentThread());这句话。
若返回false,条件是h==t,说明当前Sync队列没有初始化或者只有队首或者Thread.currentThread所处于的节点
为Sync队列的老二节点(正在等待获取资源),这几种情况都说明当前线程可以尝试对资源进行获取。

Sync队列没有初始化,完全可以参与资源的获取。
Sync只有队首和老二节点是Thread.currentThread这两种情况完全可以归纳成一种,区别就是有没有在Sync队列中而已(这点不太懂的可以去看AQS的acquireQueued函数)。
若返回true,条件是h!=t,head的下一个节点为空或者head的下一个节点不为空当时节点的线程不是当前的线程。
我们着重来看返回true条件,有点晦涩难懂,一开始看的时候。
既然h!=t,那么说明Sync队列的结点数至少有两个。s!=null&&s.thread!=Thread.currentThread()这种情况我们就不说,既然是这样,那么说明Sync队列在等待的线程数目大于1。
我们来讨论一下为什么会出现h!=t&&h.nextnull的情况,一开始你会觉得不对呀,这不科学呀。可是你要想想,这是在并发的环境下,一切又好像有可能发送。
什么情况下会出现h.next
null的情况呢?

在当前线程进入hasQueuedPredecessors if判断的同时,已经有其他的线程通过addWaiter或者enq方法改变了尾结点(tail),但是首节点的后继指针还没指向该尾节点。这时,其实队列中已经有等待的结点了。
如图:

将head赋值给当前的h,但是有其他线程执行p == head && tryAcquire(arg)将原先的头结点移出了队列,所以导致h.next为null。
以上就是ReentrantLock非公平锁和公平锁的具体实现我们来梳理一下:

非公平锁: 当线程争夺锁的过程中,会先进行一次CAS尝试获取锁,若失败,则进入acquire(1)函数,进行一次tryAcquire再次尝试获取锁,若再次失败,那么就通过addWaiter将当前线程封装成node结点加入到Sync队列,这时候该线程只能乖乖等前面的线程执行完再轮到自己了。
公平锁: 当线程在获取锁的时候,会先判断Sync队列中是否有在等待获取资源的线程。若没有,则尝试获取锁,若有,那么就那么就通过addWaiter将当前线程封装成node结点加入到Sync队列中。
通过上面的对比,我们就可以大概了解公平锁和非公平锁的具体实现了。

作者:_ostreamBaba
来源:CSDN
原文:https://blog.csdn.net/Viscu/article/details/86252371
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值