深入浅出ReentrantLock(可重入锁)(2)

简介: 深入浅出ReentrantLock(可重入锁)

tryAcquire() 一旦返回 false,就会则进入 acquireQueued() 流程,也就是基于CLH队列的抢占模式,在CLH锁队列尾部增加一个等待节点,这个节点保存了当前线程,通过调用 addWaiter() 实现,这里需要考虑初始化的情况,在第一个等待节点进入的时候,需要初始化一个头节点然后把当前节点加入到尾部,后续则直接在尾部加入节点。

代码如下:

  //AbstractQueuedSynchronizer.addWaiter()
  private Node addWaiter(Node mode) {
      // 初始化一个节点,用于保存当前线程
        Node node = new Node(Thread.currentThread(), mode);
        // 当CLH队列不为空的视乎,直接在队列尾部插入一个节点
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //如果pred还是尾部(即没有被其他线程更新),则将尾部更新为node节点(即当前线程快速设置成了队尾)
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 当CLH队列为空的时候,调用enq方法初始化队列
        enq(node);
        return node;
    }
        private Node enq(final Node node) {
        //在一个循环里不停的尝试将node节点插入到队尾里
        for (;;) {
            Node t = tail;
            if (t == null) { // 初始化节点,头尾都指向一个空节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }


  • 将节点增加到CLH队列后,进入 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)) {// 通过tryAcquire获得锁,如果获取到锁,说明头节点已经释放了锁
                    setHead(node);//将当前节点设置成头节点
                    p.next = null; // help GC//将上一个节点的next变量被设置为null,在下次GC的时候会清理掉
                    failed = false;//将failed标记设置成false
                    return interrupted;
                }
                //中断
                if (shouldParkAfterFailedAcquire(p, node) && // 是否需要阻塞
                    parkAndCheckInterrupt())// 阻塞,返回线程是否被中断
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • 如果尝试获取锁失败,就会进入 shouldParkAfterFailedAcquire() 方法,会判断当前线程是否阻塞
  /**
  * 确保当前结点的前驱结点的状态为SIGNAL
  * SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程
  * 只有确保能够被唤醒,当前线程才能放心的阻塞。
  */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
           //如果前驱节点状态为SIGNAL
           //表明当前线程需要阻塞,因为前置节点承诺执行完之后会通知唤醒当前节点
            return true;
        if (ws > 0) {//ws > 0 代表前驱节点取消了
            do {
                node.prev = pred = pred.prev;//不断的把前驱取消了的节点移除队列
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
          //初始化状态,将前驱节点的状态设置成SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

当进入阻塞阶段,会进入 parkAndCheckInterrupt() 方法,则会调用 LockSupport.park(this) 将当前线程挂起。代码如下:

// 从方法名可以看出这个方法做了两件事
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//挂起当前的线程
    // 如果当前线程已经被中断了,返回true,否则返回false
    // 有可能在挂起阶段被中断了
    return Thread.interrupted();
}

4.2 非公平锁 NonfairSync.unlock()

2.1 unlock()方法的示意图

image.png

2.1 unlock()方法详解


调用 unlock() 方法,其实是直接调用 AbstractQueuedSynchronizer.release() 操作。


进入 release() 方法,内部先尝试 tryRelease() 操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。


如果 tryRelease 成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。


一旦下一个节点的线程被唤醒,被唤醒的线程就会进入 acquireQueued() 代码流程中,去获取锁。


代码如下:

public void unlock() {
        sync.release(1);
}
public final boolean release(int arg) {
    //尝试在当前锁的锁定计数(state)值上减1,
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)//waitStatus!=0表明或者处于CANCEL状态,或者是SIGNAL表示下一个线程在等待其唤醒。也就是说waitStatus不为零表示它的后继在等待唤醒。
                unparkSuccessor(h);
               //成功返回true
            return true;
        }
        //否则返回false
        return false;
}
private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
         //如果waitStatus < 0 则将当前节点清零
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
    //若后续节点为空或已被cancel,则从尾部开始找到队列中第一个waitStatus<=0,即未被cancel的节点
        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);
    }

当然在 release() 方法中不仅仅只是将 state - 1 这么简单,-1 之后还需要进行一番处理,如果 -1 之后的 新state = 0 ,则表示当前锁已经被线程释放了,同时会唤醒线程等待队列中的下一个线程。

protected final boolean tryRelease(int releases) {
     int c = getState() - releases;
    //判断是否为当前线程在调用,不是抛出IllegalMonitorStateException异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
    boolean free = false;
   //c == 0,释放该锁,同时将当前所持有线程设置为null
    if (c == 0) {
          free = true;
          setExclusiveOwnerThread(null);
    }
    //设置state
    setState(c);
    return free;
 }
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;
            // 从后往前找到离head最近,而且waitStatus <= 0 的节点
            // 其实在ReentrantLock中,waitStatus应该只能为0和-1,需要唤醒的都是-1(Node.SIGNAL)
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null) 
            LockSupport.unpark(s.thread);// 唤醒挂起线程
}

重点:unlock最好放在finally中,因为如果没有使用finally来释放Lock,那么相当于启动了一个定时炸弹,如果发生错误,我们很难追踪到最初发生错误的位置,因为没有记录应该释放锁的位置和时间,这也就是 ReentrantLock 不能完全替代 synchronized 的原因,因为当程序执行控制离开被保护的代码块时,不会自动清除锁。



目录
相关文章
|
3天前
|
Java
ReentrantLock(可重入锁)源码解读与使用
ReentrantLock(可重入锁)源码解读与使用
|
3天前
|
安全 Java 程序员
Java并发编程:理解并应用ReentrantLock
【4月更文挑战第30天】 在多线程的世界中,高效且安全地管理共享资源是至关重要的。本文深入探讨了Java中的一种强大同步工具——ReentrantLock。我们将从其设计原理出发,通过实例演示其在解决并发问题中的实际应用,以及如何比传统的synchronized关键字提供更灵活的锁定机制。文章还将讨论在使用ReentrantLock时可能遇到的一些挑战和最佳实践,帮助开发者避免常见陷阱,提高程序性能和稳定性。
|
8月前
|
Java
并发编程——ReentrantLock
Java中提供锁,一般就是synchronized和lock锁,ReentrantLock跟synchronized一样都是互斥锁。如果竞争比较激烈,推荐lock锁,效率更高。如果几乎没有竞争,推荐synchronized。
17 0
|
Java
并发编程(六)ReentrantLock
并发编程(六)ReentrantLock
90 0
|
设计模式 SpringCloudAlibaba 安全
聊一聊 ReentrantLock 和 AQS 那点事
聊一聊 ReentrantLock 和 AQS 那点事
139 0
聊一聊 ReentrantLock 和 AQS 那点事
|
Java
Java并发编程 - AQS 之 ReentrantLock
Java并发编程 - AQS 之 ReentrantLock
110 0
|
Java
Java并发编程 - AQS 之 StampedLock
Java并发编程 - AQS 之 StampedLock
111 0
|
Java
Java并发编程 - 不可重入锁 & 可重入锁
Java并发编程 - 不可重入锁 & 可重入锁
121 0
|
Java
Java并发编程 - AQS 之 ReentrantReadWriteLock
Java并发编程 - AQS 之 ReentrantReadWriteLock
80 0
|
Java 编译器 API
Java并发编程 - Synchronized & ReentrantLock 区别
Java并发编程 - Synchronized & ReentrantLock 区别
85 0
Java并发编程 - Synchronized & ReentrantLock 区别