ReentrantLock源码解析

什么是AQS

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组 件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获 取线程的排队工作,同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步 器实现锁的语义,可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交 互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同 步器很好地隔离了使用者和实现者所需关注的领域
----------以上摘抄自《并发编程的艺术》

ReentrantLock相关类之间的关系图

在这里插入图片描述

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,我们似乎只需要了解Lock里面的方法,就可以正常使用,而对于真正的底层实现我们则是一概不知,这里我又要再次强调面向接口编程的好处,假如说有一天我们不在使用AQS保证并发问题,而是使用了另外一套机制,那么我们只需要继承Lock接口,实现好接口的每一个方法,这样对于使用者而言,根本不影响其正常使用,这也已经印证了上面说的话,锁是面向使用者的

AQS的结构(由于源码篇幅太长,就不在此贴代码了)

在AQS中,已经实现了很多正对队列和同步变量的操作的模板方法,如果程序员想要自定义同步组件,只需要继承AQS,重写其中的部分方法,就可以完成同步组件的看法
AQS一共提供两套方法,分别对应独占式的获取同步(重写tryAcquire/tryRelease)和 共享式的获取同步(重写tryAcquireShared/tryReleaseShard),至于队列的初始化,添加,移除等等一些列的操作AQS已经完成了封装

Sync的结构在这里插入图片描述

在这里插入图片描述
Sync是作为在ReentrantLock中静态内部类的存在,实现了AQS这个接口,前面说到,想要通过实现AQS去实现一个同步组件是需要重写 tryAcquire/tryRelease 或者是 tryAcquireShared/tryReleaseShard 这两组方法,对于ReentrantLock而言,正是在Sync及其子类NonfairSync和FairSync中进行了实现

下面,我将从加锁,解锁和公平锁非公平锁 三 个维度来对ReentrantLock和AQS的源码展开分析

公平锁和非公平锁

在AQS中,公平锁和非公平锁对应着Sync的两个子类NonfairSync和FairSync
首先是对于lock方法而言

非公平锁
final void lock() {
    if (compareAndSetState(0, 1))
         setExclusiveOwnerThread(Thread.currentThread());
    else
         acquire(1);
}
公平锁
final void lock() {
     acquire(1);
}

可以看到非公平锁可以说式完全不讲道理,不论自己是第几个获取锁的就直接尝试修改同步状态,只有修改失败了,才会调用acquire(1)
而公平锁则不一样,不会直接去尝试去修改同步状态

对boolean tryAcquire(int acquire)的实现也有小小的不一样

公平锁
if (c == 0) {
    if (!hasQueuedPredecessors() &&
	     compareAndSetState(0, acquires)) {
    	 setExclusiveOwnerThread(current);
         return true;
    }
}
非公平锁
if (c == 0) {
	if (compareAndSetState(0, acquires)) {
		setExclusiveOwnerThread(current);
		return true;
	}
}

这里可以看到在tryAcquire的实现中公平锁多了一个hasQueuedPredecessors()

public final boolean hasQueuedPredecessors() {
       
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}

在hasQueuedPredecessors这个方法中,
分情况讨论:h != t 节点数为0 : 可以获取锁
h !=t 节点数为1:那么这个节点只能是头节点,而且是上一个持有锁的节点,并且现在已经释放锁了
如果说这个不是上一个持有锁的节点,请看下面源码,在解锁过程中,是先获取锁,在进行头节点移除

在这里插入图片描述

加锁

以公平锁为例,从ReentrantLock的lock方法出发

ReentrantLock t = new ReentrantLock();
t.lock();			//ReentrantLock的方法
sync.lock();		//调用到公平锁的lock方法
 acquire(1);        //调用到AQS的 acquire	

对于加锁而言最核心的方法就是这个acquire方法

	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

在acquire 中的tryAcquire方法就是AQS留给子类进行重写的方法,下面进入这个方法里面,看看发生了什么

        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;
        }

首先获取到当前的线程,和AQS中的一个变量state,这个变量就表示着同步资源
判断state是不是==0,为0就表示当前的锁资源没有被抢占
就会进入hasQueuedPredecessors()这个判断,这个判断前面说过,就是去判断以下,当前线程是否需要排队
如果符合排队规则,那么就可以尝试去获取锁,但是这里不保证没有其他线程获取锁,所以在获取的时候使用了cas操作
如果成功的话就可以保存一下当前获取锁的所记录,然后返回true,那么即然获取锁成功acquire中后续的方法也就不会执行了

再提一嘴ReentrantLock是提供了重入锁功能,而这个功能的实现就在上述方法的else if里面
试想一下,如果当前线程调用的方法又涉及到这个锁的加锁操作,会再次进入到acquire这个方法中
这个时候就会进入这个else if里面,判断当前运行的线程是不是持有锁的线程
是的话就会将同步状态加1,所以再解锁的时候也会对应的进行 -1

以上都是tryAcquire成功的情况,到这里似乎还不涉及任何和队列的操作,那么当tryAcquire失败了会怎样呢?
当tryAcquire返回false的时候机会先将当前线程的信息放入一个Node节点,然后将这个Node节点放入AQS中有Node节点组成的双向的链表,具体细节接下来给大家分析

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

这个方法就是将当前线程的信息存入到一个Node节点中
首先判断这个队列的尾节点是不是为null,如果为空,说明没有队列,这里我们先讨论不为空的情况
不为空的操作还是很简单的,就是通过cas操作来将当前节点设置为尾节点,但是,这里也是可能有多线程一起设置的问题
所以这一步是可能失败的,如果失败的话就会进入到 enq(node)这个方法里面

其实这里我们发现如果尾节点为空,好像就是直接进入到这个enq(node)方法里面

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

再这个方法里面依然是判断当前AQS中的尾节点是不是为空,如果为空的话就会进行初始化,就会new一个内容为空的Node节点,那么为什么需要new 一个空的Node节点呢??
首先当前线程能走到这一步就是说明当前线程是加锁失败了的,而现在这个锁正在被别的线程使用,这里new 的空的Node其实就是代表这个正在持有锁的线程!但是这同样有可能存在多个线程同时的去new 这个Node所以也使用了cas操作
如果说new 这个空Node 失败了也没有关系,这里使用了一个死循环,再进入第二遍循环的时候,就会尝试获取尾节点的位置,然后将当前节点设置为为尾节点,失败了也没有关系,会循环再来一遍,直道成功为止

当我们设置尾节点成功之后就会进入acquireQueued方法,注意,在这个时候县城其实还是在运行,线程并没有被阻塞

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在这个acquireQueued方法里面就会完成上面没有完成的工作,就是对线程进行阻塞
同样用到了一个死循环,先获取当前节点的前一个节点,如果说当前线程的前一个节点是头节点,那么会在这里再一次尝试加锁
如果成功了就直接返回,如果失败了则进入下一个if,这里提前透露以下,判断头节点这个操作跟线程唤醒是有关系的

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
}

在这个方法里面会判断当前线程节点的前一个节点的状态,这里要说明一个,一个节点在创建之后,如果没有任何修改,那么就表示默认状态,但是再这里,会将当前线程节点的前一个节点的状态改为 -1,这个状态表明,这个节点的后面的所有节点都是等待状态,然后return,但是现在依然没有将线程阻塞,由于再acquireQueued方法中使用的是一个死循环,所以还会进入到上述方法里面,这一次的话判断当前线程节点的前一个线程的状态为-1就会返回true,之后就会调用后续的 parkAndCheckInterrupt 完成线程的阻塞

不知道说到这里大家有没有一个疑问,我们刚才讲了那么一大堆的操作,假设当前线程的阻塞刚好再原持有锁线程之后,那么线程不久永远也不会被唤醒了??
当然不会,我们进入到 parkAndChectInterrupt 方法,会发现在这里的线程阻塞使用到的是 LockSupport.park(this); 这个方法
这个方法有一个特点,就是如果阻塞和解阻塞的顺序可以反过来,也就是说,我们可以先进行unpark,再进行prak,线程一样是能正常运行的,这也就解决了我上述的问题

补充:
在acquireQueued方法里面,我们还能看到一个interrupt变量,看到这个我们很容易联想到中断标志位,没错,就是中断标志位,进入到
parkAndCheckInterrupt()这个方法里面,我们能发现返回的是Thread.interrupted(),这就是说在acquireQueued方法返回的是一个中断标志位,直接导致的结果就是,再次调用Thread.currentThread().interrupt(),这也在线程红调用的申明了InterruptedException方法就会抛出异常进行中断,例如线程在lock的代码块里调用了sleep

解锁

解锁的调用过程类似于前面讲的加锁,这里不再详细分析
解锁的核心方法是tryRelease`

	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交给子类重写的方法,在这个方法中重写了一些解锁相关的规则

	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);
            }
            setState(c);
            return free;
        }

首先将同步变量-1,因为ReentrantLock支持重入锁,所以在这里-1了并不能说明就已经解锁完成了,只有当state这个标志位为0的时候,才是 解锁成功的时候
解锁成功后会设置持有锁线程为null
然后就会吊用unparkSuccessor

 private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

这里采用从后往前的遍历方法,找到头节点的下一个节点,然后对这个节点执行唤醒操作
折下来的跳跃性可能有点大了,还记得之前的线程是在那个方法里进行park的么

 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

没错正是这个acquireQueued,被唤醒后线程会再次进入循环之中,此时的tryAcquire就可以成功获取到锁了!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值