ReentrantLock 加锁解锁原理

简介

ReentrantLock是Java的JUC工具包下一个重要的类,相对于synchronized,它有如下特点:

  • 可中断(在获取锁失败后,阻塞等待锁的过程,这个阻塞状态,是可以被打断的,打断后会抛出异常)
  • 可超时(有时限的等待)
  • 可公平(可以设置为公平锁,让来获取线程的锁先到先得)
  • 支持多个条件变量(synchronized对对象加锁,关联操作系统的一个Monitor,一个Monitor只有1个WaitSet,所有wait的线程都到这个WaitSet中等待,而ReentrantLock相当于可以有多个WaitSet)

ReentrantLock和synchronized都支持可重入。

基本用法是创建ReentrantLock对象,该对象调用lock()获得锁,unlock()释放锁。

public static void main(String[] args){
    //获取 ReentrantLock对象
    ReentrantLock reentrantLock = new ReentrantLock();
    //获取锁
    reentrantLock.lock();
    try{
        //...
    } finally{
        //释放锁
        reentantLock.unlock();
    }
}

调用lock()获得锁,是不可打断的,也无超时,如果竞争锁失败,会一直阻塞等待。lockInterruptibly()是可打断的。tryLock(long timeout, TimeUnit unit)可设置超时等待。tryLock()无参数是立即返回结果,获得到锁就返回true,获得不到也不阻塞,直接返回false。

源码时间

lock()

进入ReentrantLock的lock()方法

public void lock() {
        sync.lock();
    }

再进sync.lock(),这里看非公平实现

NonfairSync有这四个关键属性:

  • exclusiveOwnerThread是持有锁的线程,state、head、tail都来自其继承的父类AQS(AbstractQueuedSynchronizer),state是一个状态值,head和tail都是节点指针。

lock()方法首先通过cas将state从0改为1,0表示无锁,1表示有锁,如果成功,设置当前线程为exclusiveOwnerThread。如果失败,进入acquire(1)。

cas是为了保证原子操作,避免出现线程并发安全问题。比如有一个共享变量count=0,现在用cas将其修改为1,在修改时,会去看看count值是不是还是原来的0,如果是,证明没有被其它线程修改过,那么可以成功改为1;如果不是,证明被其它线程修改过了,那么这次cas操作失败,放弃修改。

acquire(1)

首先走tryAcquire(1),尝试获得锁,如果成功,方法结束。

tryAcquire(1)

看tryAcquire内部逻辑,同样是看非公平实现。

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


    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();    //获取state值
            if (c == 0) {    //如果state为0,无锁状态
                if (compareAndSetState(0, acquires)) {   //尝试cas将state从0改为1
                    setExclusiveOwnerThread(current);  //cas成功,设置当前线程为owner
                    return true;
                }
            }
          //如果已经有线程加锁了,并且当前线程是owner,证明这是重入
            else if (current == getExclusiveOwnerThread()) {
                //将state值+1
                int nextc = c + acquires;    
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
  • 如果还没有线程加锁,或者加锁的就是自己这个线程,那么tryAcquire(1)是成功的,否则失败。

tryAcquire()失败,会接着走acquireQueued(addWaiter(Node.EXCLUSIVE), 1),相当于让线程进入阻塞队列,具体是用NonfairSync中的head和tail指针连接双向链表实现。

addWaiter(null)

Node.EXCLUSIVE是null。这是第一次进addWaiter()方法,链表还是空的。head和tail也是指向null。

    private Node addWaiter(Node mode) {
        //创建当前线程节点
        Node node = new Node(Thread.currentThread(), mode);
        //链表为空,当前pred为null,不走if
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //进入enq(node)    
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
        for (;;) {    //死循环
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

t指针指向链表尾指针tail,满足t==null,compareAndSetHead(new Node())创建一个节点,让head指向它。

cas成功,【tail = head】,tail和head指针一起指向这个新节点,第一次循环结束。第二次循环开始,【Node t = tail;】t指针和tail指针一起指向新节点,这次t != null了,进入else块,【node.prev = t;】 让node的头指针指向新节点,cas让链表尾指针tail指向node,cas成功后,【t.next = node;】让新节点的尾指针指向node,最后返回t指向的新节点。

经过这次enq(),链表结构变成:head --> node(null) <--> node(thread) <- tail

第一次进入addWaiter()方法,即链表为空时,会创建两个节点,一个是哑节点(占位节点),一个是实际节点(有对应线程的节点)。后面进入addWaiter()方法,都是创建一个实际节点。

enq()结束,回到addWaiter()方法,返回当前线程节点。

acquireQueued(node,1)

node为当前线程节点

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {    //死循环
                final Node p = node.predecessor();    //获取node前驱节点
                //前驱节点是head指向节点(占位节点),证明当前节点是链表中第一个
                //有实际线程对应的节点,tryAcquire(1)再次尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    //成功获得到锁,让head指向node,即让node作为新的占位节点,把node中的
                    //线程信息清空,node头指针指向null
                    setHead(node);
                    //将前驱节点next指针指向null
                    p.next = null; // help GC
                    //上两行代码等同于将原来的占位节点从链表中移除,让node取而代之。
                    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;
    }

如果能够成功获取到锁,线程就可以成为owner了,这个node也没用了,去做占位节点。tryAcquire如果失败或者不是第一个有效节点,执行第二个if块内容,第一次进shouldParkAfterFailedAcquire(),会把前置节点状态设置为-1,表示它有责任去唤醒它的后继节点(因为它的后继节点拿不到锁要准备阻塞等待了),返回false,然后回到acquireQueued()继续循环,如果是第一个有效节点,又有一个tryAcquire尝试获取锁的机会,失败,又到shouldParkAfterFailedAcquire(),第二次进,就返回true了,就会执行parkAndCheckInterrupt(),线程在此阻塞等待。

来数数看,线程在cas获取锁失败后,一共还有几次tryAcquire()尝试获取锁的机会,acquire中1次,如果是链表中第一个竞争失败节点,acquireQueued中有2次,所以在阻塞前,它最多会尝试3次。(事不过三啊)acquireQueued过程没有被打断并且成功拿到了锁,返回false,acquire()结束,lock()结束,如果被打断了,最后拿到锁返回true,回到acquire(),把自己打断。

unlock()

解锁流程基于以下情况:

Thread-0持有锁,Thread-1、Thread-2、Thread-3来获取锁都失败阻塞,并且head指向的占位节点及Thread-1、Thread-2的status值都为-1,表示有责任唤醒它的后继节点。

现在,Thread-0调用unlock()方法释放锁。

ReentrantLock的unlock()方法,调用同步器的release(1)

release(1)

release(1)在AQS中

tryRelease(1)

进入tryRelease(1),选择以下实现

如果有锁重入,(一个线程加了多次锁)没解完,返回false。只剩一把锁,解一次解完了,返回true。

    protected final boolean tryRelease(int releases) {
            //获取state值并-1
            int c = getState() - releases;
            //校验调用解锁方法的线程是否是拥有锁的线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();

            boolean free = false;
            //解锁成功,设置owner为null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //状态值存入
            //如果state减完后为0,解锁成功,返回true。
            //如果不为0,证明有锁重入,还需要多次解锁,返回false。
            setState(c);
            return free;
        }

这里假设Thread-1没有锁重入,解锁成功,tryRelease(1)返回true。回到release(1)中,因为head != null 并且head的status为-1,所以调用unparkSuccessor(head),将head指向的占位节点的status改为0,并唤醒后继节点,即unpark第一个有效节点,也就是Thread-1所在节点。

unparkSuccessor(node)
    private void unparkSuccessor(Node node) {
        //获取head状态
        int ws = node.waitStatus;
        //head状态为-1,进入这个if
        if (ws < 0)
            //将head指向节点的status改为0
            compareAndSetWaitStatus(node, ws, 0);
        
        //拿到Thread-1所在节点
        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)
            //Thread-1被唤醒
            LockSupport.unpark(s.thread);
    }

还记得线程获取锁失败是在哪阻塞的吗?

那当然是在获取锁的地方啦,就是acquireQueued()中的parkAndCheckInterrupt()处

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

现在Thread-1被唤醒了,它就接着走循环,【node.predecessor()】获取它的前驱节点,就是head,所以满足if了,进入tryAcquire(1)尝试获取锁,在没有竞争的情况下,Thread-1就成功拿到锁了,把owner设置为Thread-1,进入【if (p == head && tryAcquire(arg)) {}】这个if块,将Thread-1原来所在的节点设置为头节点(新的占位节点),并把节点内的线程信息清空,原来的占位节点就被移除出链表了。(原本是head --> 占位 <--> node,在setHead(node)中让node.prev = null,在if块中燃p.next = null,等于把占位节点的前后指向都断开了)

于是,变成了酱紫:

如果Thread-1被唤醒后有竞争,即有其他线程来跟他抢着加锁,不巧,Thread-1又没抢过,那么它又在acquireQueued中循环尝试三次,又再次park阻塞住了。(当然,这时之前的占位节点就保住它的地位啦)

### Java ReentrantLock 底层实现机制 #### 锁的核心概念 `ReentrantLock` 是基于 `AbstractQueuedSynchronizer (AQS)` 实现的一种可重入互斥锁。它支持公平和非公平两种模式,并且允许线程多次获取同一把锁而不会发生死锁[^3]。 #### AQS 的核心作用 AQS 提供了一个框架用于构建依赖于 FIFO 等待队列的阻塞锁和其他同步器。`ReentrantLock` 利用了 AQS 的独占模式,通过维护一个 `volatile int state` 变量来记录锁的状态。该变量表示当前锁被持有的次数[^4]。 #### 加锁过程 (`lock()` 方法) 当调用 `lock()` 方法时,`ReentrantLock` 会尝试获取锁。以下是其主要逻辑: 1. **状态检查** 调用 `nonfairTryAcquire(int acquires)` 方法检查锁是否已被其他线程持有。如果没有被占用,则尝试使用 CAS 操作设置 `state` 并标记当前线程为锁拥有者[^5]。 2. **可重入性处理** 如果锁已经被当前线程持有,则增加 `state` 计数以反映锁的嵌套级别。每次调用 `unlock()` 方法都会减少计数值,只有当计数归零时才会真正释放锁。 3. **排队等待** 若锁被其他线程持有且无法立即获取,则当前线程会被加入到 AQS 维护的 CLH 队列中并进入阻塞状态,直到轮到自己再次尝试获取锁[^1]。 #### 解锁过程 (`unlock()` 方法) 解锁操作相对简单,主要是减少 `state` 计数并唤醒下一个等待线程。具体流程如下: 1. 减少 `state` 值; 2. 当前线程确认不再持有锁后,通知后续节点重新竞争资源; 3. 如果有多个线程处于等待状态,按照 FIFO 或指定策略依次唤醒它们。 #### 公平与非公平锁的区别 - **非公平锁**:不考虑线程到达顺序,在任何情况下都可能直接尝试抢占锁,即使已有线程正在等待。这种方式提高了吞吐量但可能导致某些线程长期得不到执行机会(即饥饿现象)。 - **公平锁**:严格按照请求先后次序分配锁给线程,虽然更公正但也带来了额外开销,降低了整体性能[^2]。 ```java public class TestReentrantLock { private final ReentrantLock lock = new ReentrantLock(); public void testMethod() { lock.lock(); // 获取锁 try { System.out.println(Thread.currentThread().getName() + " acquired the lock."); } finally { lock.unlock(); // 释放锁 } } public static void main(String[] args) throws InterruptedException { TestReentrantLock test = new TestReentrantLock(); Thread t1 = new Thread(test::testMethod, "Thread-1"); Thread t2 = new Thread(test::testMethod, "Thread-2"); t1.start(); t2.start(); t1.join(); t2.join(); } } ``` 上述代码展示了如何创建以及管理基本的 `ReentrantLock` 实例,并演示了多线程环境下的锁定行为。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值