ReentrantLock 原理

1、概述

ReentrantLock 主要利用 CAS + AQS队列来实现,通过重写了 AQS 的 tryAcquire 和 tryRelease方法实现的 lock 和 unlock。

  • Sync:抽象类,是 ReentrantLock 的内部类,继承自 AQS,实现了释放锁的操作(tryRelease()方法),并提供了 lock 抽象方法,由其子类实现。
  • NonfairSync:是 ReentrantLock的内部类,继承自 Sync,非公平锁的实现类。
  • FairSync:是 ReentrantLock 的内部类,继承自 Sync,公平锁的实现类。

AQS内部维护着一个 FIFO(先进先出) 队列,该队列就是 CLH 同步队列。AQS 依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS 则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到 CLH 同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

ReentrantLock 的基本实现可以概括为:先通过 CAS 尝试获取锁。如果此时已经有线程占据了锁,那就加入 AQS 队列并且被挂起。当锁被释放之后,排在 CLH 队列队首的线程会被唤醒,然后 CAS 再次尝试获取锁。在这个时候,如果:

  • 非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
  • 公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

2、源码分析

2.1 构造函数

public class ReentrantLock implements Lock, java.io.Serializable{ 
    // 默认非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    //指定是当前是公平锁 还是非公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

2.2 非公平锁源码分析

2.2.1 加锁

    public void lock() {
        //当为非公平锁时 这里的lock会走NonfairSync下的lock函数
        sync.lock();
    }
    
    final void lock() {
            //利用CAS更新当前锁的状态
            if (compareAndSetState(0, 1))
                //设置当前线程为独占锁的拥有者
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //如果CAS更新失败 获取锁
                acquire(1);
    }

    //如果CAS更新成功,设置当前线程为独占锁的拥有者
    //AbstractOwnableSynchronizer中方法;
    //AbstractOwnableSynchronizer把AQS包了一层;主要提供了一些获取、设置当前独占锁拥有者
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

首先用一个 CAS 操作,判断 state 是否是0(表示当前锁未被占用),如果是 0 则把它置为 1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。

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

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }

    final boolean nonfairTryAcquire(int acquires) {
        //获取当前线程
        final Thread current = Thread.currentThread();
        //获取state变量值
        int c = getState();
        if (c == 0) { //没有线程占用锁
            if (compareAndSetState(0, acquires)) {
                //占用锁成功,设置独占线程为当前线程
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁,重入锁与原理
            // state++
            int nextc = c + acquires;
            if (nextc < 0) {
                throw new Error("Maximum lock count exceeded");
            // 更新state值为新的重入次数
            setState(nextc);
            return true;
        }
        //获取锁失败
        return false;
    }

非公平锁 tryAcquire 的流程:

  1. 检查state字段,若为 0,表示锁未被占用,那么尝试占用
  2. 若不为 0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。
  3. 如果以上两点都没有成功,则获取锁失败,返回false。

acquire流程:

  1. 调用自定义同步器的 tryAcquire() 尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则 addWaiter() 将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued() 使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源,获取到资源后才返回,返回false。如果在等待过程中被中断过,那么会从park() 中醒过来,发现拿不到资源,从而继续进入 park() 等待,等到轮到自己时才返回 true。
  4. 如果线程在等待过程中被中断过,会调用cancelAcquire()方法取消当前节点的状态,如果其前置节点释放了锁,当前取消节点还要唤醒后续节点,到了acquire方法,当前节点只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

2.2.2 解锁

    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;
    }
    protected final boolean tryRelease(int releases) {
        // 计算释放后state值
        int c = getState() - releases;
        // 如果不是当前线程占用锁,那么抛出异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            // 锁被重入次数为0,表示释放成功
            free = true;
            // 清空独占线程
            setExclusiveOwnerThread(null);
        }
        // 更新state值
        setState(c);
        return free;
    }

tryRelease 的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。  

release流程:尝试释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

2.2.3 总结

调用 lock 方法,当为非公平锁时,只要有线程过来就尝试获取锁,

  • 如果获取成功
    • AQS的state==0 并且CAS写入成功,将自身设置为独占锁的拥有者
    • state != 0 但当前独占锁就是自身(表示重入),将state累加;
  • 如果获取失败就将自己设置到 AQS 队列的尾部,等待唤醒;

2.3 公平锁源码分析

当为公平锁时,与非公平锁的主要区别在于公平锁在执行 tryAcquire 时,需要加一个判断(当前节点是否还有其他节点),如果有其它节点则将自身添加到队列中等待唤醒;

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 先检查 AQS 队列中是否有前驱节点, 没有才去竞争,非公平锁是直接竞争
            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;
    }

    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        // h != t 时表示队列中有 Node
        return h != t &&
            // (s = h.next) == null 表示队列中还有没有老二
            ((s = h.next) == null || 
            // 或者队列中老二线程不是此线程
            s.thread != Thread.currentThread());
    }

2.4 可重入原理

    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)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 支持锁重入, 只有 state 减为 0, 才释放成功
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

2.5 可打断原理 

2.5.1 不可打断模式

默认是不可打断模式,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。

    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)) {
                    setHead(node)
                    p.next = null;
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    } 

2.5.2 可打断模式

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    // AQS 内部方法
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            // 如果没有获得到锁, 进入
            doAcquireInterruptibly(arg);
    }

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                // 在 park 过程中如果被 interrupt 会抛出异常
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

### 回答1: ReentrantLockJava中的一个锁类,它是一个可重入锁,允许同一个线程多次获得同一个锁。在使用ReentrantLock时,我们需要显式地获取锁和释放锁,可以通过lock()和unlock()方法来完成这些操作。 ReentrantLock采用了一种非公平的获取锁的方式,这意味着当多个线程同时请求锁时,ReentrantLock并不保证锁的获取顺序与请求锁的顺序相同。这种方式的好处是可以减少线程竞争,从而提高系统的并发性能。 另外,ReentrantLock还支持Condition条件变量,可以使用它来实现线程的等待和通知机制,以及更加灵活的线程同步和通信。 总之,ReentrantLockJava中一个非常强大的锁类,可以帮助我们实现高效的线程同步和并发控制。但是,使用ReentrantLock也需要注意一些问题,比如需要正确地使用try-finally块来释放锁,避免死锁等问题。 ### 回答2: ReentrantLockJava中的一种可重入锁,它提供了与synchronized关键字相似的功能,但具有更强大的扩展性和灵活性。 ReentrantLock内部使用一个同步器Sync来实现锁机制。Sync是ReentrantLock的核心组件,它有两个实现版本,分别是NonfairSync和FairSync。 NonfairSync是默认的实现版本,它采用非公平方式进行线程获取锁的竞争,即线程请求锁的时候,如果锁可用,则直接将锁分配给请求的线程,而不管其他线程是否在等待。 FairSync是公平版本,它按照线程请求锁的顺序来分配锁,当锁释放时,会优先分配给等待时间最长的线程。 ReentrantLock在实现上使用了Java的锁机制和条件变量来管理线程的等待与唤醒。当一个线程调用lock方法获取锁时,如果锁可用,线程会立即获得锁;如果锁被其他线程占用,调用线程就会被阻塞,进入等待队列。 当一个线程占用了锁之后,可以多次重复地调用lock方法,而不会引起死锁。这就是ReentrantLock的可重入性。每次重复调用lock都需要记住重入次数,每次成功释放锁时,重入次数减1,直到次数为0,锁才会被完全释放。 与synchronized相比,ReentrantLock提供了更多的高级功能。例如,可以选择公平或非公平版本的锁,可以实现tryLock方法来尝试获取锁而不会阻塞线程,可以使用lockInterruptibly方法允许线程在等待时可以被中断等等。 总之,ReentrantLock通过灵活的接口和可重入特性,提供了一种强大的同步机制,使多个线程可以安全地访问共享资源,并且具有更大的灵活性和扩展性。它在并发编程中的应用非常广泛。 ### 回答3: ReentrantLock是一种与synchronized关键字相似的线程同步工具。与synchronized相比,ReentrantLock提供了更灵活的锁操作,在并发环境中能更好地控制线程的互斥访问。 ReentrantLock原理主要包含以下几个方面: 1. 线程控制:ReentrantLock内部维护了一个线程的等待队列,每个线程通过调用lock()方法来竞争锁资源。当一个线程成功获取到锁资源时,其他线程会被阻塞在等待队列中,直到锁被释放。 2. 重入性:ReentrantLock允许同一个线程多次获取锁资源,而不会发生死锁。这种机制称为重入性。在线程第一次获取到锁资源后,锁的计数器会加1,当该线程再次获取锁时,计数器会再次加1。而在释放锁时,计数器会递减。只有当计数器减为0时,表示锁已完全释放。 3. 公平性和非公平性:ReentrantLock可以根据需要选择公平锁或非公平锁。在公平锁模式下,等待时间最久的线程会优先获取到锁资源。而在非公平锁模式下,锁资源会被直接分配给新到来的竞争线程,不考虑等待时间。 4. 条件变量:ReentrantLock提供了Condition接口,可以创建多个条件变量,用于对线程的等待和唤醒进行管理。与传统的wait()和notify()方法相比,Condition提供了更加灵活的等待和通知机制,可以更加精确地控制线程的流程。 总的来说,ReentrantLock是通过使用等待队列、重入性、公平性和非公平性、条件变量等机制,来实现线程的互斥访问和同步。它的灵活性和粒度更高,可以更好地适应各种复杂的并发场景。但由于使用ReentrantLock需要手动进行锁的获取和释放,使用不当可能会产生死锁等问题,因此在使用时需要仔细思考和设计。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小鲁蛋儿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值