• ReentrantLock 源代码之我见


    ReentrantLock,英文意思是可重入锁。从实际代码实现来说,ReentrantLock也是互斥锁(Node.EXCLUSIVE)。与互斥锁对应的的,还有共享锁Node.SHARED
    ReentrantLock 集成了Lock接口,Lock接口主要功能有上锁lock()、尝试上锁tryLock()、规定时间内尝试上锁tryLock(long time, TimeUnit unit)、释放锁unlock()。

    ReentrantLock有个内部的抽象类Sync,这个Sync类继承了AbstractQueuedSynchronizer类,内部定义了抽象上锁方法lock(),还有非公平尝试上锁nonfairTryAcquire(int acquires),
    尝试释放锁tryRelease(int releases) 、是否持有互斥锁isHeldExclusively()等方法。
    Sync 有两个子类,非公平锁NonfairSync和公平锁FairSync。两个子类,都实现了抽象的方法上锁lock(),同时还有一个尝试上锁tryAcquire(int acquires)。在Sync的子类实现中,这个
    tryAcquire(int acquires)的形参acquires都是1,表示加锁1次。这个加锁次数,维护在AQS里面的变量state中,这个后面会讲。

    ReentrantLock 类内部,还有上锁lock()、尝试上锁tryLock()、规定时间内尝试上锁tryLock(long time, TimeUnit unit)、释放锁unlock()、获取加锁次数getHoldCount()

    获取等待的条件hasWaiters()等。其中,最重要,也是最常用的,是lock()、unlock()、tryLock()这些。
    ----------------------------------------------------------------------------------------------------------------

    挑主要的方法来讲。

    先介绍上锁lock()。
     public void lock() {
            sync.lock();
        }

    在这个方法中,sync.lock(),是一个策略模式,由子类的实现而确定。如果子类是公平锁FairSync,则调用FairSync的lock()方法;否则,调用非公平锁

    NonfairSync的lock()方法。

    先看公平锁的lock(),代码如下

    final void lock() {
                acquire(1);
            }
    //加锁
    public final void acquire(int arg) {
    //如果尝试上锁上锁,并且获取队列成功,则当前线程自中断。
    if (!tryAcquire(arg) && //这里的tryAcquire,由子类实现,如下面的代码2。从这里可以看出,非公平所,acquire获取锁的时候,会直接尝试获取锁。失败则加入资源队列
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt(); // 自中断
    }

    //通过自旋的方式获取同步状态。所谓自旋,说白了,就是死循环for(;;)。这个方法返回中断状态
    //代码1
    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)) { //如果前驱节点是头节点并且获取锁成功,则把头节点设置成当前的节点。并且把前驱节点的next设置为null,方便gc。这里再次调用了tryAcquire
                    setHead(node);
    p.next = null; // help GC //注意这里的写法。因为当前节点已经成为头部节点,当前节点的线程关闭后,当前节点也会被回收。那么当前节点的前驱节点的next,需要设置成null,否则gc不会回收当前节点。
    failed = false;
    return interrupted; //返回当前的节点的中断状态:false
    }
    if (shouldParkAfterFailedAcquire(p, node) && //是否应该挂起失败的线程
    parkAndCheckInterrupt())
    interrupted = true;
    }
    } finally {
    if (failed)
    cancelAcquire(node);
    }
    }

    //是否应该挂起失败的线程
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    //如果前驱节点,已经是SIGNAL,也就是-1,那么直接返回true,表示可以挂起。因为,前驱节点释放锁后,会唤醒后续的的节点
    if (ws == Node.SIGNAL)
    /*
    * This node has already set status asking a release
    * to signal it, so it can safely park.
    */
    return true;
    if (ws > 0) { //如果前驱节点已经被注销,也就是waitStatus > 0(大于0 的只有被注销的状态),则执行下面的循环
            /*
    * Predecessor was cancelled. Skip over predecessors and
    * indicate retry.
    */
    do { //这里不断循环,直到前驱节点的状态<=0。当等于0的时候,表示没有状态。当小于0的时候,有-1 -2 -3三种情况。其中,-3是共享模式才有,表示节点可传播。-2则是表示节点是处于条件Condition队列。-1才表示节点处于等待队列。
    node.prev = pred = pred.prev; //其实就是常用的for循环的变种而已
            } while (pred.waitStatus > 0);
    pred.next = node;
    } else {
    /*
    * waitStatus must be 0 or PROPAGATE. Indicate that we
    * need a signal, but don't park yet. Caller will need to
    * retry to make sure it cannot acquire before parking.
    */
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //采用CAS操作,设置前驱节点的状态为-1,表示释放锁后,会唤醒后驱节点
    }
    return false;
    }
    /挂起并设置中断
    private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
    }

    //代码4
    //节点取消获取锁
    private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
    return;

    node.thread = null;

    // Skip cancelled predecessors
    //这里跳过前驱节点。这里的实现挺巧妙的
    Node pred = node.prev;
    while (pred.waitStatus > 0) //如果前驱节点的状态>0,也就是已经被取消了,则循环向前查找前驱节点,直到前驱节点的状态 < = 0,也就是SIGNAL -1状态或者PROPAGATE -2传播状态。传播状态只在共享模式下才有用
    node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next; //前驱节点的后驱节点

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED; //设置当前节点状态为取消,也就是1

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) { //如果当前节点是末尾节点,当前节点n会被设置成CANCEL,则把等待队列的末尾节点设置成当前节点的前驱节点,也就是第n-1个节点被设置成了末尾节点
    compareAndSetNext(pred, predNext, null); //将前驱节点的后驱节点设置为null,因为当前节点已经设置成了CANCELLED了,前驱节点正式成为末尾节点,也就不会再由后驱节点
    } else {
    // If successor needs signal, try to set pred's next-link
    // so it will get one. Otherwise wake it up to propagate.
    int ws;
    if (pred != head && //这里的说明,在下面说明1处详述
    ((ws = pred.waitStatus) == Node.SIGNAL ||
    (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
    pred.thread != null) {
    Node next = node.next; //当前节点的后驱节的成下一个节点
    if (next != null && next.waitStatus <= 0)
    compareAndSetNext(pred, predNext, next); //前驱节点的后驱节点本来是当前节点,现在将前驱节点的后驱节点,设置成当前节点的下一个节点。
    } else { //直接后激活驱节点。park是挂起unpark是激活
    unparkSuccessor(node);
    }

    node.next = node; // help GC //将节点的后驱节点设置成自身,方便gc。这里要注意的是,不同于其他变量,设置成null
    }
    }
    //说明1:如果当前节点不是头节点且线程不是空,有以下几种场景:1、前驱节点的状态已经是唤醒状态SIGNAL -1, 2、如果前驱节点不是SINGAL,会可能=0(没有状态) 或者=-3(共享模式的传播状态),则设置前驱节点的状态为SIGNAL

    //代码5
    //激活后驱节点(使后驱节点可用)
    private void unparkSuccessor(Node node) {
    /*
    * If status is negative (i.e., possibly needing signal) try
    * to clear in anticipation of signalling. It is OK if this
    * fails or if status is changed by waiting thread.
    */
    int ws = node.waitStatus;
    if (ws < 0) //如果当前节点的状态<0,即唤醒SINGAL -1状态 ,或者等待条件CONDITION -2状态 ,或者PROPAGATE -3 传播状态(共享模式),则将节点的状态设置成0(没有状态)
    compareAndSetWaitStatus(node, ws, 0);

    /*要激活的线程通常是在后驱节点上持有(这句话怎么意思?我的理解是,当前节点的后驱节点持有的线程,会被激活。也就是后驱节点的线程,会变成可用状态)。
    *如果后驱节点已经被取消或者被设置成null,则从末尾节点开始往前搜索,直接找到一个不是null又不是取消的节点。
    * Thread to unpark is held in successor, which is normally
    * just the next node. But if cancelled or apparently null,
    * traverse backwards from tail to find the actual
    * non-cancelled successor.
    */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
    s = null;
    for (Node t = tail; t != null && t != node; t = t.prev) //这里从末尾节点开始循环,直到当前节点的下一个非CANCEL&非null的节点,可以参考下图
    if (t.waitStatus <= 0)
    s = t;
    }
    if (s != null)
    LockSupport.unpark(s.thread); //使后驱节点持有的线程可用。但是只是使线程可用,不保证线程会被执行。
    }
    
    
    
    //代码2
    protected
    final boolean tryAcquire(int acquires) {
    //先拿到当前线程
    final Thread current = Thread.currentThread();
    //获取上锁的次数
    int c = getState(); if (c == 0) { //如果上锁次数为0,则证明锁空闲 if (!hasQueuedPredecessors() && //如果没有前驱节点Node,则证明当前节点是头节点。使用CAS方法,设置上锁次数。这个的次数,保存在AQS的state里面 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); //设置锁的持有者为当前线程 return true; } } else if (current == getExclusiveOwnerThread()) { //如果锁由当前线程持有,则上锁次数+acquires。这个acquires总是1 int nextc = c + acquires; if (nextc < 0) //校验参数合法行 throw new Error("Maximum lock count exceeded"); setState(nextc); //设置加锁次数 return true; } return false; }

    公平锁,总是先选择第一个节点加锁。如果锁已经被当前线程持有,当前线程再次获取锁的时候,会成功,加锁次数+1。这里体现的,就是ReenTrantLock的可重入性。

    下面介绍释放锁的方法。事实上,公平锁和非公平锁的释放,都是调用了父类Sync的方法

    public void unlock() {
            sync.release(1);  //这里调用的是父类AQS的释放锁的方法
        }
    
    //释放锁
    public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; //因为是公平锁,永远是头节点获取到锁,也就永远从头节点开始释放锁 if (h != null && h.waitStatus != 0) unparkSuccessor(h); //代码5的激活后驱节点线程 return true; } return false; }


    尝试释放锁,调用的是父类Sync的方法,如下:

    protected final boolean tryRelease(int releases) {
                int c = getState() - releases;         //加锁次数叠减。这里的releases总是1
                if (Thread.currentThread() != getExclusiveOwnerThread())   //如果释放锁的线程不是排他锁的持有线程,则抛出异常
                    throw new IllegalMonitorStateException();
                boolean free = false;
                if (c == 0) {
                    free = true;
                    setExclusiveOwnerThread(null);        //如果加锁次数已经是0,则设置锁的持有现场为null
                }
                setState(c);          //设置加锁次数
                return free;
            }

    下面介绍非公平锁

    final void lock() {
                if (compareAndSetState(0, 1))   //先采用 CAS操作尝试获取锁,成功则把当前线程设置成排他(互斥)锁线程
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);                //否则,执行加锁操作。这里的加锁操作acquire(1),和公平锁的代码一模一样。唯一的区别,是加锁时候调用的tryAcquire,各自实现而已。
            }
    
     protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);    //这里的nonfairTryAcquire ,直接是调用父类Sync的方法。
    }
    //非公平锁尝试获取锁。由此可见,ReentrantLock的默认锁,是非公平锁。

    final
    boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); //获取当前线程 int c = getState(); //确认加锁次数。加锁次数维护在超类AQS的state里。这个state 是由volatile里(注意这个volatile内存言语的作用,是用于共享变量在多线程即时可见。
    //也就是一个线程改变了state,另一个线程马上能够看见。这个内存言语,是实现并发的基础之一)
    if (c == 0) { //加锁次数为0,证明锁还没有被获取 if (compareAndSetState(0, acquires)) { //CAS操作,加锁。这里的acquires在ReentrantLock里,总是1 setExclusiveOwnerThread(current); //设置当前线程持有排他锁 return true; } } else if (current == getExclusiveOwnerThread()) { //如果加锁次数大于1,且是当前线程持有锁,则加锁次数累加 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); //设置加锁次数 return true; } return false; }

    由上面的公平锁和非公平锁的实现可以看到,实现大同小异,都是调用超类AQS的 acquire(int arg) 方法(acquire(int arg)是一个模板方法模式代码)。

    不同的是,公平锁总是第一个节点才能获取到锁,这里并不符合计算机的资源最大使用思想。而非公平锁,则是由jvm调度。因此ReentrantLock默认使用的是非公平锁。

    公平锁和非公平锁,都有各自的tryAcquire方法

    ReentrantLock实现的基础,是AQS的虚拟双向队列CLH,具体表现在代码里,则是Node节点。AQS的队列有两种,一种是资源队列(用于唤醒等操作),一种条件队列(用于条件达到Condition)

    Node节点,在AQS里面,是由volatile这个关键字,volatile同时又是内存言语。volatile的修饰,可以使功节点对于不同的线程即时可见。这是关键字,是并发的基础之一。

    当一个线程想获取锁,被阻塞的时候,表现在代码里面,就是一个死循环for(;;),直到当前线程所在的节点获取到锁。

    这里是类似于监听事件的原理:利用Node节点的修饰符volatile的特性。当另一个节点a(线程)释放了锁的时候,另一个线程b马上可以检测到。如果是节点b是节点a的后驱节点,则节点b可以获取到锁,而b的后驱节点c

    则需要等待b释放锁后,再通知后驱节点c。这样c节点的线程,就实际形成了阻塞。

     ---------------------------------------------------------------

    个人水平有限,请各位大佬指点。 

    
    
  • 相关阅读:
    get_json_object 用法
    vim中的特殊字符
    vim常用命令
    mac下如何配置用户的环境变量
    vim如何替换^M ?
    git ssh 配置
    mac上pip install时提示/System/Library/... 无权限
    mysql语句--table中某列有多值时,删除其他,仅保留1条
    mysql语句如何把多行的数据合并到一行?
    微服务分布式电商项目学习笔记(二)---- 微服务架构图+微服务划分图(2020/7/1)
  • 原文地址:https://www.cnblogs.com/drafire/p/14387037.html
Copyright © 2020-2023  润新知