深入理解ReentrantLock源码

ReentrantLock

1、概念和基本原理

1、可重入性

可重入是指同一个线程可以多次获取同一把锁。例如,在一个方法中获取了ReentrantLock锁,然后在这个方法内部调用了另一个也需要获取该锁的方法,此时线程可以成功获取锁,而不会被阻塞。这就好比一个人有一把自己房间的钥匙,他可以多次进入自己的房间,每次进入相当于线程对锁的一次获取。

2、独占锁性质

ReentrantLock是独占锁,在同一时刻,只有一个线程可以获取该锁。当一个线程获取了锁之后,其他线程如果也想获取这把锁,就会被阻塞,直到锁被释放。这就像是一个只有一个入口的房间,一次只能有一个人进入。

3、实现原理

​ 它通过一个内部的同步器(AbstractQueuedSynchronizer,简称 AQS)来实现锁的获取和释放机制。AQS 维护了一个等待队列,当线程获取锁失败时,会被封装成一个节点加入到等待队列中等待锁的释放。当锁被释放时,AQS 会按照一定的规则(公平或非公平)唤醒等待队列中的线程来尝试获取锁。

2、使用方法

1、基本操作

class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;
    public int increment() {
        lock.lock();
        try {
            count++;
            return count;
        } finally {
            lock.unlock();
        }
    }
}

首先需要创建ReentrantLock对象,例如:ReentrantLock lock = new ReentrantLock();。然后通过lock.lock()方法来获取锁。如果锁当前没有被其他线程持有,那么获取锁的线程可以立即获得锁并继续执行后续代码。如果锁已经被其他线程持有,那么当前线程会被阻塞,直到获取到锁为止。当线程完成了对共享资源的操作后,需要通过lock.unlock()方法来释放锁。如果不释放锁,其他等待该锁的线程将永远无法获取锁,这可能会导致程序出现死锁的情况。

公平锁与非公平锁

公平锁

ReentrantLock可以通过构造函数参数来指定是否为公平锁。公平锁的特点是,等待时间最长的线程会优先获取锁。例如:ReentrantLock fairLock = new ReentrantLock(true);创建了一个公平锁。在公平锁模式下,线程获取锁的顺序遵循先来后到的原则,等待队列中的线程按照 FIFO(先进先出)的顺序获取锁。

非公平锁

​ 如果不指定构造函数参数或者传入false,则创建的是一个非公平锁,这也是ReentrantLock的默认模式。非公平锁的优势在于它的性能通常比公平锁要好。如果刚好锁处于可用状态,那么当前线程就可以直接获取锁,而不必等待队列中的线程先获取,不会引起线程的上下文切换。例如:ReentrantLock unfairLock = new ReentrantLock(false);或者ReentrantLock unfairLock = new ReentrantLock();创建的是非公平锁。

3、源码解释

构造方法

    /**
     * 默认构造方法
        默认使用非公平锁。从上面的公平锁和非公平锁的对比克制
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * 指定是否使用公平锁
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

3.1 ReentrantLock加锁原理

    /**
     * 非公平锁的实现
     */
  static final class NonfairSync extends Sync {

        /**
         * 首先尝试加锁,加锁失败才会进入阻塞队列
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
    }
   /**
     * 公平锁的实现方式
     */
    static final class FairSync extends Sync {

        final void lock() {
            acquire(1);
        }
    }

从上面两个类的源码可知,公平锁和非公平锁都会调用acquire()方法

   /**
     * tryAcquire()尝试加锁
     * acquireQueued()进入队列
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //重新设置线程中断标识,因为acquireQueued里面调用了 Thread.interrupted()方法清除了标志位
            selfInterrupt();
    }

下面我们看下 tryAcquireacquireQueued这两个方法。

static final class FairSync extends Sync {
    
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //当前无线程加锁
            if (c == 0) {
                //判断队列中 prev 是否有执行者
                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;
        }
    }
 //非公平锁的实现方式
  final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //无线程竞争,直接尝试加锁
                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;
        }

由此可见公平锁和非公平锁的的区别。公平锁的在加锁之前判断队列中是否有元素存在,如果不存在,才会尝试加锁。而非公平锁,则是直接进行加锁。

下面我们看下线程是如何加入阻塞队列中

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;
    }
// 将元素插入队列,必要的时候进行链表的初始化
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //初始化tail 和 head 指针。这里对 head 指针进行加锁,因为此时 head 指针不能被释放掉。如果 head 被释放锁之后,这个链表的后续都不能连接起来。
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //首先将线程节点链接到 prev 节点上。
                node.prev = t;
                //正常情况下,线程节点 cas 成功会将 tail 指针指向线程节点
                if (compareAndSetTail(t, node)) {
                    //如果这个时候 tail 节点已经释放,将会 cas 失败,那么t.next = node就无法赋值
                    t.next = node;
                    return t;
                }
            }
        }
    }

能够进入到该方法说明 线程节点已经进入到队列中,先尝试一下判断该节点是否已经释放,

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //因为锁唤醒的时候,不一定能拿到锁。所以这里进行了死循环
            for (;;) {
                final Node p = node.predecessor();
                //如果该节点的前一个节点不是 head,就没有必要进行 tryAcquire().因为轮不到该节点来释放
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //判断前面的节点是否取消,用于判断当前线程节点能否安全的去阻塞。如果前一节点已经是取消状态,自己的睡眠就不安全。因为无法被唤醒操作。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //阻塞方法,并且判断是否是中断
                    parkAndCheckInterrupt())
                    //是中断唤醒
                    interrupted = true;
            }
        } finally {
            // tryAcquire是用户自定义的方法,如果这块内容出现了异常,则需要在这里进行兜底操作
            if (failed)
                cancelAcquire(node);
        }
    }

方法参数: Node pred 前一节点,Node node 是当前节点

Node.SIGNAL = -1 : 正常等待状态。如果Node.waitStatus = SIGNAL。则后面的线程一定会被唤醒,有责任去唤醒其他的后续节点

Node.CANCELED = 1 :取消状态,也就是无效状态

Node.CONDITION= -2 : 表示在某个条件下 线程阻塞

Node.PROPAGATE= -3 :表示传播。如果当前线程节点是Node.PROPAGATE,则进行下一个节点的唤醒操作

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 如果前一个节点的状态是正常等待状态,线程节点的状态设置成等待即可
             */
            return true;
        if (ws > 0) {
            /*
             * 如果前一个节点状态是取消状态,则进行遍历前面的节点,知道找到正常等待的节点
             */
            do {
               // 这里跳过的节点,为什么不需要手动回收?
                // 因为跳过的节点,没有 GC Root,垃圾回收器会自动回收
                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.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt()方法

 private final boolean parkAndCheckInterrupt() {
        //unsafe操作。线程阻塞
        LockSupport.park(this);
        //判断线程是否中断了,因为阻塞队列中的线程节点,无法响应中断
        return Thread.interrupted();
  }

Thread.interrupted(); 方法作用: 获取线程的标志位,判断是否被中断。检查的时候,会清除标志位,设置成 0

Thread.interrup() 方法作用: 给线程信息设置 标志位

cancelAcquire(node) 方法

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
        //将线程信息回收。因为这个地方是线程节点需要取消操作
        node.thread = null;

        // 前面有取消的节点,这里帮助链表把取消的节点取消掉。
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
    
        Node predNext = pred.next;
    
        node.waitStatus = Node.CANCELLED;

        // 如果是 tail 指针,这里的直接释放本节点
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // 如果本节点之后有个新的入队的节点。
            int ws;
            //如果不是头节点
            if (pred != head &&
                //如果前面的节点是可唤醒的
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                //除了判断状态,还需要判断 thread 的信息是否为空。因为上面的代码可以到,node.thread 是先设置为 null。后面设置 node.waitStatus 的状态。这个时候有可能被其他的线程操作,导致信息变更
                pred.thread != null) {
                //设置下一个节点
                Node next = node.next;
                //这里帮助链接,没有进行死循环进行。
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                //如果是头节点,直接唤醒
                unparkSuccessor(node);
            }
            //将本身自己的节点指向本节点,用于自动取消操作
            node.next = node; // help GC
        }
    }

3.2 ReentrantLock释放锁

 /**
     * 
     */
    public final boolean release(int arg) {
        //释放本节点
        if (tryRelease(arg)) {
            Node h = head;
            //唤醒后续节点,伪唤醒机制,因为这里只是尝试去唤醒后续节点,不一定能够成功。
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

unparkSuccessor() 方法

// 这里的 Node 是 head 节点
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)
            //当前节点的 head,正在唤醒其他的节点,head 节点就不需要进行唤醒了。
            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) {
            //如果后续节点是空,则尝试从后往前唤醒。在入队的时候,当 tail 指针 cas 成功之后,next 指针关联失败的情况。这里对这种情况进行弥补操作。
            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);
    }

线程的操作方法

unpark()

  • unpark()方法用于为指定线程提供一个许可。如果目标线程当前正被park()方法阻塞,那么这个许可会让该线程立即解除阻塞状态并继续执行。如果目标线程还没有调用park()方法,那么这个许可会被保存起来,等到线程调用park()方法时,它会发现许可已经可用,就不会被阻塞

park()

  • park()是一种用于阻塞线程的机制。当一个线程调用park()方法时,它会检查一个许可(permit)是否可用。如果许可不可用,线程将被阻塞,进入等待状态,直到获得许可或者被中断。
  • 许可的初始状态是不可用的。线程在调用park()方法时,就好像在等待一个信号来继续执行。这个信号就是许可。如果没有其他线程调用unpark()方法(稍后介绍)来提供许可,线程就会一直处于阻塞状态

总结:

两者谁先谁后调用都无所谓两者谁先谁后调用都无所谓

喜欢的可以关注我的公众号 程序员茶馆
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大伟攀高峰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值