并发编程-ReentrantLock加锁解锁源码详细解析

基础 专栏收录该内容
8 篇文章 0 订阅

AQS是什么

当我们构造一个ReentrantLock对象的时候,可以通过传入一个布尔值来指定公平锁还是非公平锁,进入源码可以发现区别是new了不同的sync对象

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

点进去FairSync或者NonfairSync的源码可以发现,其实它们都间接继承了AbstractQueuedSynchronizer类,该抽象类为我们的加锁解锁过程提供了模板方法,所以我们先来了解下它

AbstractQueuedSynchronizer,简称AQS,为构建不同的同步组件(重入锁,读写锁,CountDownLatch等)提供了可扩展的基础框架,如下图所示。

在这里插入图片描述
AQS的内部主要是构造了一个先进先出的双向队列,把抢锁失败的线程放入队列中并阻塞该线程,等锁释放后再唤醒线程.我们先来看看AQS的内部结构(只展示ReentrantLock中用到的)

//头结点
private transient volatile Node head;
//尾巴节点
private transient volatile Node tail;
//状态变量  加锁就是对这个变量CAS
private volatile int state;
static final class Node {
	//节点等待状态 
	volatile int waitStatus;
	//上一个节点
	volatile Node prev;
	//下一个节点
	volatile Node next;
	//节点的线程
	volatile Thread thread;
	Node nextWaiter;
}

明白了AQS的基本结构后,其实我们就可以先大概猜测一下

加锁: 对state变量进行CAS,失败的话则新建一个node对象,把自己放到head的后面,然后此时又拿不到锁,没啥事做,那就自闭吧,把线程阻塞起来,防止cpu空转.
解锁: 同样对state变量进行CAS,然后再通知其他线程来抢锁,最重要的是要把之前阻塞的线程唤醒,然后有个线程重新获得锁,周而复始.

ok,进入源码验证下我们的猜测吧

加锁源码

ReentrantLock一般是使用非公平锁,所以我们先看看非公平锁的源码

final void lock() {
            //直接CAS加锁看能否成功
            if (compareAndSetState(0, 1))
                //成功后设置当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

上面直接先尝试CAS,如果成功后把占用锁的线程设置成自己,加锁失败则进入acquire方法

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //补偿中断状态
            selfInterrupt();
    }

tryAcquire方法作用

首先来看看tryAcquire方法里面做了什么,点进去会发现最终的实现是nonfairTryAcquire方法

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //获取锁状态
            int c = getState();
            if (c == 0) {//直接尝试加锁,这里和外层的尝试加锁是一样的,只是再尝试一次
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    //加锁成功的话就直接返回true了,外层的acquire方法其实就直接结束了
                    return true;
                }
            }
            //判断是否是当前线程持有锁
            else if (current == getExclusiveOwnerThread()) {
                //此处可以说明是可重入锁
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //返回false说明需要放入队列
            return false;
        }

可以发现tryAcquire做了两件事: 1,再次尝试加锁 2,持有锁的是否是自己 如果return
true的话,方法就直接结束了,如果reture
false的话,则会进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg),

addWaiter方法

那我们先来看下addWaiter方法

 private Node addWaiter(Node mode) {
        //此时mode为null
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //tail是结尾的指针,赋值给pred,当第一个线程进来的时候,tail为null
        Node pred = tail;
        //此处判断pred是否为null就是判断tail是否为null,也就是判断队尾是否有节点

        //如果不为空,直接把当前节点放到tail的后面
        if (pred != null) {
            //把当前节点放在tail后面
            node.prev = pred;
            //把当前节点设置为tail指针,其实就是把当前节点设置成最后一个
            if (compareAndSetTail(pred, node)) {//CAS确保入队时是原子操作
                //当前node成为新的队尾
                pred.next = node;
                return node;
            }
        }
        //如果为空,执行下面的方法
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
        //死循环初始化队列
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //死循环的第一次会初始化tail和head,刚开始的时候其实tail和head是一样的
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //这里的代码和addWaiter中的是一样的
                node.prev = t;
                //死循环的第二次会把当前node接到之前的tail位置后面
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

可以发现addWaiter方法的作用其实就是把当前node设置到tail节点的后面,如果tail节点为空的话则执行enq方法初始化head和tail节点,无论结果如何,addWaiter都会返回当前的node

acquireQueued方法

然后下一步进入acquireQueued方法

final boolean acquireQueued(final Node node, int arg) {
        //标志1
        boolean failed = true;
        try {
            //标志2
            boolean interrupted = false;
            //死循环
            for (;;) {
                //找到当前node的prev节点
                final Node p = node.predecessor();
                //我们可以把head当做当前持有锁的节点,如果前一个是head,则开始重新获取锁
                if (p == head && tryAcquire(arg)) {
                    //如果此时获取锁成功,则把当前节点放到head位置
                    setHead(node);
                    //node代替了之前的head,所以把之前的head置为null
                    p.next = null; // help GC
                    failed = false;
                    //加锁成功
                    return interrupted;
                }
                //有两种情况会到这
                //1,前一个不是head,说明还有其他兄弟在排队
                //2,前一个是head, 但是head还没有释放锁,加锁失败了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

有两种情况会去执行shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法
1,前一个不是head,说明还有其他兄弟在排队
2,前一个是head, 但是head还没有释放锁,加锁失败了
通过方法名称可以知道shouldParkAfterFailedAcquire是检测当前线程是否有挂起的资格
parkAndCheckInterrupt则是说明在shouldParkAfterFailedAcquire返回true的情况下,挂起线程

shouldParkAfterFailedAcquire方法

首先来看下shouldParkAfterFailedAcquire方法

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //上一个节点的状态
        int ws = pred.waitStatus;
        //如果是SIGNAL,直接返回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) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                //如果是CANCELLED状态,循环往队列前面找,直到找到一个SIGNAL的节点,然后把当前节点放到SIGNAL节点的后面
                node.prev = pred = pred.prev;
            } 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.
             */
            //把之前的节点设置为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

可以发现shouldParkAfterFailedAcquire方法的作用就是保证当前node节点的prev节点的状态必须是SIGNAL,当满足这个条件后才会去执行parkAndCheckInterrupt方法,否则就会进入下一次死循环直到保证
prev节点的状态是SIGNAL,因为只有当prev节点是SIGNAL状态时,后续才会去唤醒下一个节点,当前节点才敢把自己挂起,

parkAndCheckInterrupt方法

然后我们看下parkAndCheckInterrupt方法

private final boolean parkAndCheckInterrupt() {
        //阻塞当前线程
        LockSupport.park(this);
        //这里返回true后会清除掉状态
        return Thread.interrupted();
    }

这里就是直接挂起自己,等待head节点把自己唤醒,到这里的话,非公平锁的加锁过程就结束了
可以看到和我们之前的猜测基本一致,非公平锁的实现我们已经了解了,那么公平锁的实现和非公平锁有啥区别呢

公平锁是怎么实现公平的呢?

我们看看公平锁和非公平锁两者源码的区别

区别1

//公平锁
 final void lock() {
            acquire(1);
        }
//非公平
 final void lock() {
            //直接CAS加锁看能否成功
            if (compareAndSetState(0, 1))
                //成功后设置当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

上面我们对比了lock方法 如果是非公平锁,新进来一个线程会直接去尝试加锁,根本不会排队 公平锁则直接进入了acquire方法

区别2

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

上面是公平锁的tryAcquire方法,我们可以看到这个和非公平锁的区别只是多了一个hasQueuedPredecessors()判断方法,同样,我们先看看这个方法是干啥的

hasQueuedPredecessors方法的作用

 public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        //判断队列中是否有优先级更高的等待线程
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        //这里如果返回false 才会去CAS抢锁,否则就去排队
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

这个方法是判断队列中是否有优先级更高的等待线程 返回true:有优先级更高的等待线程,当前线程乖乖去排队
返回false:没有优先级更高的线程, 直接去CAS抢锁

然后我们来分析下
h != t && ((s = h.next) == null || s.thread != Thread.currentThread());

h != t有哪些情况?

  • 情况1: head为null, tail不为null
  • 情况2: head不为null, tail为null
  • 情况3: head和tail都不为null且不相等

从enq方法可以看到head是先于tail设置的,所以情况1是不存在的

然后来到下一步

((s = h.next) == null || s.thread != Thread.currentThread());

情况2: 这时其他线程进入enq方法刚执行完compareAndSetHead(new Node()),但是还没有给tail赋值 此时(s= h.next) == null 为 true,直接退出循环,此时说明有优先级更高的线程在执行任务
情况3: 队列已经初始化成功, 此时(s= h.next) == null 为 false, 然后判断当前节点是不是head节点的下一个节点,请注意之前已经给s = h.next赋值了,如果 s.thread != Thread.currentThread()说明当前节点也不是第二个节点,那么就退出循环,乖乖排队去

发现没,(s = h.next) == null这个判断其实就是区分上面的情况1和情况2的,只是过于简洁导致不太好看懂

总结一下,实现公平锁就是通过hasQueuedPredecessors方法来判断是否有高优先级的线程,而不是像非公平锁一样直接去抢锁.

提示下大家, 无论非公平锁还是公平锁,都是在线程没入队列之前操作的, 但是只要入了队列就必须乖乖排队

总结一下加锁流程

线程尝试加锁,加锁失败后会进入AQS队列,队列第一次会初始化一个head节点,此时head节点的内部线程是null,然后第一个加入队列的线程节点thread1会接在初始化后的head节点后面,然后thread1会park自己,也就是说,除了head节点外,队列中其他的节点都会park自己,然后持有锁的线程释放锁后,会唤醒head节点后面的第一个节点,此时也就是thread1会被唤醒,thread获取到锁后会把自己置为head,并且把自身的thread置为null,因为拿到锁后会setExclusiveOwnerThread(current);所以没有必要再持有线程.
除了第一次初始化的head外,所有的head节点都是已经拿到锁的节点

解锁源码

解锁的话相对来说就比较简单了

	public void unlock() {
        sync.release(1);
    }
    
	public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

release方法的流程基本就是对state做操作,然后如果队列有下一个节点,则去唤醒下一个节点

tryRelease方法

先看下tryRelease方法

		protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            //拥有锁的线程才能解锁
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //c == 0才算解锁成功, 也就是说加几次锁必须解几次锁
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

unparkSuccessor唤醒线程

再看下是怎么唤醒下一个线程的

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.
         */
        //这里的状态有 0:初始化状态  -1 :就是之前的SIGNAL状态 1:CANCELLED状态
        int ws = node.waitStatus;
        if (ws < 0)
            //把当前状态置为初始状态0
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 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;
            //从队列最后往前遍历,找到离head节点最近的状态为SIGNAL的节点,然后唤醒
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //如果不为null,直接唤醒下一个线程,下一个线程会把自己重新设置成head
        if (s != null)
            LockSupport.unpark(s.thread);
    }

为啥从后往前遍历队列?

这里有个问题,为啥这里的队列要从后往前遍历呢?
既然这么写,那肯定是因为如果从前往后遍历会出现某种问题,那么会有啥问题呢?
我们假设有这样一种场景

现在有A,B,C三个线程在执行任务
线程A当前已经排在了任务队列的最后面,也就是tail节点
1,此时线程B执行了入队的enq方法中的compareAndSetTail(t, node),但是还没有执行t.next=node的时候,
2,线程C执行了compareAndSetTail(t, node)并且执行了t.next = node;这时候会怎么样?

当线程B所在的节点入队后,但是还没有执行t.next=node的时候,队列是这样的
此时已经把tail设置给了线程B所在的node2节点,但是此时还没有设置node1的next节点
在这里插入图片描述

此时cpu的时间片分给了线程C,线程C开始执行入队任务并且执行了t.next = node,此时的队列是这样的
在这里插入图片描述
发现了没,这个时候node1节点的next还是为null,所以在这种情况下从前往后遍历的话,队列就断了,
这里可以看到每个节点的prev都是能找到节点的,所以这里要从后往前遍历

结尾

有啥理解不当之处希望大家能指出来,有问题的话欢迎一起讨论~

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

紫枫231

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值