ReentrantLock加锁分析

1、ReentrantLock中其实是有一个AQS的子类实例的成员变量sync;

2、实际是调用的Sync中的lock;Sync是AQS的子类;Sync有两个子类,公平与非公平;默认为非公平;如下是非公平加锁分析;

    public ReentrantLock() {
        sync = new NonfairSync();
    }

3、cas进行加锁:0->1,成功即加锁成功,并进行业务逻辑处理,然后解锁;

compareAndSetState(0, 1)

4、cas加锁:0->1 会有两种情况导致失败:

  1. 锁被其它线程占用;
  2. 已经被当前线程(自己)占用;
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

5、cas失败,则acquire(1);

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

 如果上图的if判断不通过,则代表 p!=head 或者 当前线程再次尝试还是没有拿到锁。

那么可以确定:1、队列不为空,当前线程的前置线程节点还没有出队列且没有竞争锁成功;2、锁依旧被其它线程占用着;

 如上两个条件的加持下,就可以确定当前加入的线程节点一定是在将来可以被唤醒的,从而避免了并发情况下解锁时唤醒后置节点-后置节点为空,遗漏线程无法被唤醒的情况。

6、进行tryAcquire(1);其中会再一次进行cas设置,进行锁重入逻辑判断并处理;

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 如果此时锁被释放了,再进行一次cas设置
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    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;
            }
            return false;
        }

        如上代码,我曾还怀疑是否存在 setState(nextc); 线程安全的问题?

        心想着在else if (current == getExclusiveOwnerThread()) 判断成立之后,锁被释放,并且被其它线程重新获取锁成功。然后再进行 int nextc = c + acquires; setState(nextc); 就会存在线程不安全问题。

        然后仔细琢磨,current == getExclusiveOwnerThread() 既然成立,说明持有当前锁的线程就是正在执行当前方法逻辑的线程,那么既然当前线程正在执行tryAcquire逻辑,又怎么可能在这个时候去释放锁,所以这里是线程安全的。

7、如果 !tryAcquire(arg) 再次cas没有成功并且不是进行多次重入加锁,那么就会进行将当前线程构建成Node并加入队列的操作;

8、addWaiter(Node.EXCLUSIVE),创建一个等待Node,并且这个Node是一个互斥独占的类型;

    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    addWaiter(Node.EXCLUSIVE)

    Node node = new Node(Thread.currentThread(), mode);

        Node(Thread thread, Node mode) {
            // 这样就很方便的知道了当前队列是共享型阻塞队列,还是独占型阻塞队列
            this.nextWaiter = mode;
            this.thread = thread;
        }
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {
            // 表示队列之前已经被初始化过
            node.prev = pred;
            // 如果入队cas没有成功则会进行enq(node)
            if (compareAndSetTail(pred, node)) {
                // 这里node入队列尾部,同样使用cas做了并发安全控制
                pred.next = node;
                return node;
            }
        }
        // 将节点插入队列,如有必要进行初始化
        enq(node);
        return node;
    }

9、将节点插入队列,如有必要进行初始化。enq(node); 如下操作会一直自旋直到把node插入队列为止

    private Node enq(final Node node) {
        for (;;) {
            // 这里就必须要把node搞进队列为止了,如果一次不行则两次......
            Node t = tail; // 所以tail必须为volatile修饰
            if (t == null) { // Must initialize
                // 队列初始化
                if (compareAndSetHead(new Node()))
                    // 虚拟一个Node,设置为头(不包含线程)
                    // 如果不成功则下次for循环
                    tail = head;
            } else {
               // 队列已经存在node
                node.prev = t;
                if (compareAndSetTail(t, node)) { // 将新增的node设置为尾
                    t.next = node;
                    return t;
                }
            }
        }
    }

10、acquireQueued(node,1);这个方法中包含三种情况的逻辑;

  1. 当前线程的Node的pre就是head,并且再次尝试cas:0->1成功,那么就需要将当前线程的Node设置为head,并且将原先的head进行出队列;(如果当前线程想要去竞争锁,那么前提条件是当前线程的Node必须是head
  2. 当前线程的Node的pre不是head或者cas:0->1失败,那么就需要将当前线程进行park让其阻塞,等待被释放锁的线程唤醒;
  3. Node对应的线程被唤醒后,重新执行for循环判断,cas获取锁;
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取当前线程Node的前驱节点
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    // 当前线程获取锁成功,及当前参数Node中的线程就是当前线程
                    // 并且当前Node是此时队列中的第二个
                    // 那么代表第一个Node已经使用完锁,需要将其进行从队列中移除
                    // 当前线程的Node设置为队列的头
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 进行线程阻塞操作
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) // 阻塞当前线程,被唤醒后判断当前线程释放被中断
                    interrupted = true; // 阻塞被唤醒后并且当前线程已经被中断
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

11、shouldParkAfterFailedAcquire(Node pred, Node node),当前线程Node的前驱节点的状态是SIGNAL(前驱节点有义务唤醒它的下一个)时,才会将当前线程进行park操作;

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            // 当前驱节点被赋予责任后(需要唤醒后续节点)
            return true;

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

12、parkAndCheckInterrupt(),进行当前线程park操作;

    private final boolean parkAndCheckInterrupt() {
        // 这里会进行当前线程阻塞,直到被唤醒
        LockSupport.park(this);
        // 检查被唤醒的线程释放已经被中断
        return Thread.interrupted();
    }

13、会有两种情况线程会被唤醒继续执行 return Thread.interrupted(); 之后的逻辑

  1. 被park阻塞后的线程被唤醒,并且目标线程被中断,那么上面方法会返回true;
  2. 被park阻塞后的线程被唤醒,目标线程未被中断;

在Java中,unsafe.park(false, 0) 方法与传统的可中断方法(如 Thread.sleep() 等)不同,它并不会抛出 InterruptedException 异常来响应中断。实际上,unsafe.park(false, 0) 方法是一种非阻塞的线程挂起方式,它可以响应中断但并不会抛出异常。

当使用 unsafe.park(false, 0) 方法时,如果线程被中断,它会在被唤醒后继续执行,此时需要通过检查中断标志位来判断线程是否被中断,并进行相应的处理。因此,这种方式下的线程挂起是一种可响应中断的行为,但并不会抛出 InterruptedException 异常。

总结起来,unsafe.park(false, 0) 方法可以响应中断,但不会抛出异常。

14、被唤醒的线程再次cas尝试获取锁,如果获取锁失败会继续进行park,循环往复,直到获取锁成功。

好文推荐:

AQS源码分析(四)[lock流程图总结与unlock源码分析]_哔哩哔哩_bilibili

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进窄门见微光行远路

如果对你有比较大的帮助

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值