万字长文解析ReentrantLock源码

ReentrantLock 特征

特点:

1.可重入
2.公平/非公平
3.可中断
4.支持条件等待
5.可设置锁超时

常用 API

使用例子:

public class ReentrantLockTest {

    static ReentrantLock lock = new ReentrantLock(true);

    static class ClientThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread() + "开始尝试获取锁");
            lock.lock();
            try {
                System.out.println(Thread.currentThread() + "成功获取锁");
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread() + "完成释放锁");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ClientThread t1 = new ClientThread();
        ClientThread t2 = new ClientThread();
        ClientThread t3 = new ClientThread();
        t1.start();
        t2.start();
        t3.start();

        TimeUnit.SECONDS.sleep(10);
    }
}

源码分析

获取锁

如果我使用下面的代码进行获取就行:

ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock();

ReentrantLock 默认调用的就是非公平锁 调用栈: java.util.concurrent.locks.ReentrantLock#lock

  • java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
final void lock() {
    // 直接尝试加锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 如果获取锁失败进入 AQS acquire 逻辑
        acquire(1);
}

如果 compareAndSetState(0, 1)能够直接执行成功,那么将直接结束方法的执行。 如果失败,那么就会调用acquire 方法如下:

public final void acquire(int arg) {
    // tryAcquire(arg) 尝试获取锁
    // acquireQueued 获取锁失败进行等待队列
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

我们先看 tryAcquire方法: java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire

  • java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire

他会直接调用到 nonfairTryAcquire非公平锁的加锁逻辑 里面有两个逻辑:

  • 如果当前状态无锁,直接尝试加锁,加锁成功返回 true
  • 如果当前时锁重入,那么直接修改 AQS 状态共享变量值 state 等于 c + acquires, 加锁成功返回 ture
  • 如果都不满足,那么返回加锁失败返回 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;
        }
    }
    // 当前线程持有锁, state 计数 +1
    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;
}

如果 tryAcquire 调用完成后是获取锁成功 acquire方法执行结束,最后代表 lock 方法执行结束。

获取锁失败进入同步队列

如果获取锁失败,那么就会执行 acquire代码后面段 if 逻辑的执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 这里其实可以分为两个方法来看

  • addWaiter(Node.EXCLUSIVE)
  • acquireQueued(xxx, arg)

按照执行顺序,我们先看 addWaiter(Node.EXCLUSIVE) 这里主要是入队的逻辑。 addWaiter: java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter

private Node addWaiter(Node mode) {
    // 将当前线程转换为 AQS Node 节点
    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;
        // cas 修改 tail 节点,如果成功返回 node 
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果失败,调用 enq
    enq(node);
    return node;
}

enq是将当前节点插入队列,必要的时候会进行初始化

//将节点插入队列,必要时进行初始化。
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 如果没有尾节点,那么需要进行初始化
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } 
        // 如果有尾节点/其实就是有头节点/已经被初始化,通过 CAS 入队
        else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

前面我们看看完了,当前获取锁的线程当获取锁失败的时候,成功进入 AQS 队列,接下来我们继续看 acquireQueued又做了什么呢?

  • 如果是队列头节点,会再次尝试获取锁
  • 如果修改 java.util.concurrent.locks.AbstractQueuedSynchronizer.Node状态位
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 设置为 头节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 判断是否需要进行阻塞当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 阻塞线程
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 是否失败
        if (failed)
            // 如果失败,取消获取锁
            cancelAcquire(node);
    }
}

上面我们可以看到,for (;;)中有两个判断

  • 如果是头节点,就调用tryAcquire尝试获取锁 (之前我们已经分析过 tryAcquire 了,我们主要看后面个 if )
  • 如果不是就进入 shouldParkAfterFailedAcquire 方法

在调用 acquireQueued这个过程中可能调用多次 shouldParkAfterFailedAcquire 方法。shouldParkAfterFailedAcquire 会执行一下几个操作。

  • 可以用来修改当前节点的状态,
  • 和对链表上无效的节点出队
/** 
 * 当获取锁失败后, 检查更新新节点状态如果是需要阻塞返回, true
 * <p>
 * 一个前继节点 waitStatus = 0, 第一次将继续设置为 SIGNAL, 告诉当前线程准备进入阻塞, 此时依旧获取不到, 当前线程进入阻塞
 *
 * @param pred 前继节点
 * @param node 当前节点
 * @return {@code true} if thread should block
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; // 前继节点的状态, 第一次进入的话, 一定是 0
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            // 出队, 剔除无效的节点
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 第一次进来, pred.waitStatus = 0 执行这个分支
        // 将前继节点的状态修改为 SIGNAL, 表示 pred.next 节点需要被唤醒(此时准备进入阻塞, 但是还未被阻塞, 再次获取锁失败之后才会被阻塞)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

当 Node 被修改 Node.SIGNAL状态后,第一个 if 返回 true , 我们再次回到 acquireQueued 方法,就会执行 parkAndCheckInterrupt 方法,就是将当前的线程 park 然后返回当前线程的中断状态。

private final boolean parkAndCheckInterrupt() {
    // 阻塞线程
    LockSupport.park(this);
    // 返回线程中断状态
    return Thread.interrupted();
}

注意:这里线程 park 过后,其实获取锁就结束了前半段的操作,完成同步队列的入队,并且进入等待。我们就需要等待解锁唤醒。

释放锁

释放锁的代码如下:

lock.unlock();

释放锁做了什么呢?

  • 释放当前锁的状态
  • 在 AQS 队列中去唤醒排队的头节点

调用栈如下: java.util.concurrent.locks.ReentrantLock#unlock

  • java.util.concurrent.locks.AbstractQueuedSynchronizer#release

我们可以从 release方法开始

// 解锁
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        // 判断是否有需要唤醒的线程
        if (h != null && h.waitStatus != 0) //waitStatus 的值为 0, 只有当后继存在节点才会被设置为该值不为 0, 此时需要唤醒后继线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

释放锁,主要是调用 tryRelease, 首先就是考虑之前的重入问题,直接对 state 进行 -1 ,然后如果 c == 0表示当前线程不再持有锁,我们就可以修改 ownerThread == null . 这个时候,最后修改 state 为新值。

// tryRelease 
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 判断是否是当前线程持有锁
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 如果 state == 0 表示当前线程不在占有该锁
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

释放锁成功后,再次回到 release方法,会再次判断,如果 AQS 队列不为空,那么就进行排队线程唤醒。 主要是调用 java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor

// 唤醒队列中的线程
private void unparkSuccessor(Node node) {
    // 将当前节点状态修改为 0  
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 反向查找
    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);
}

其实这里最关键的就是 LockSupport.unpark(s.thread); 这里就会回到 acquireQueued,执行唤醒后强锁的逻辑,依然在 acquireQueued里面。

释放锁后唤醒等待节点

当前节点被唤醒逻辑,首先会在 shouldParkAfterFailedAcquire 方法中出队,然后尝试加锁如果加锁成功就返回 true.

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;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        // 出队的逻辑
        do {
            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;
}

再次竞争锁,主要是在acquireQueued方法中调用 tryAcquire方法进行获取锁。如果获取锁失败,就又再次获取锁,如果获取锁成功返回。

测试和实践

支持锁中断

如果通过 lock.lockInterruptibly();_ 方式加锁,如果当前线程出现中断过后,会抛出 _java.lang.InterruptedException线程中断异常,所以 ReentrantLock支持可中断。 相关源码:

/**
 * Convenience method to park and then check if interrupted
 *
 * @return {@code true} if interrupted
 */
private final boolean parkAndCheckInterrupt() {
    // LockSupport.park 会清除中断信号
    LockSupport.park(this);
    return Thread.interrupted();
}


// 
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;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 抛出中断异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

实验代码:

public class ReentrantLockTest {

    static ReentrantLock lock = new ReentrantLock(true);

    static class ClientThread implements Runnable {

        @SneakyThrows
        @Override
        public void run() {
            System.out.println(Thread.currentThread() + "开始尝试获取锁");
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread() + "成功获取锁");
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread() + "完成释放锁");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ClientThread(), "t1");
        Thread t2 = new Thread(new ClientThread(), "t2");
        Thread t3 = new Thread(new ClientThread(), "t3");
        t1.start();
        t2.start();
        // 锁中断
        //lock.lockInterruptibly();
        TimeUnit.SECONDS.sleep(1);
        t3.start();
        TimeUnit.SECONDS.sleep(1);
        t3.interrupt();



        TimeUnit.SECONDS.sleep(10);
    }
}

输出结果:

Thread[t1,5,main]开始尝试获取锁
Thread[t2,5,main]开始尝试获取锁
Thread[t1,5,main]成功获取锁
Thread[t3,5,main]开始尝试获取锁
Exception in thread "t3" java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at io.zhengsh.juc._1lock.reentrantlock.ReentrantLockTest$ClientThread.run(ReentrantLockTest.java:18)
    at java.lang.Thread.run(Thread.java:748)
Thread[t1,5,main]完成释放锁
Thread[t2,5,main]成功获取锁
Thread[t2,5,main]完成释放锁

获取锁设置超时

lock.tryLock(2, TimeUnit.SECONDS)可以支持设置获取锁的超时时间,可以有效的避免线程饥饿问题 测试代码:

public class ReentrantLockTryTest {
    static ReentrantLock lock = new ReentrantLock(true);

    static class ClientThread implements Runnable {

        @Override
        public void run() {
            System.out.println(Thread.currentThread() + "\t" + (System.currentTimeMillis() / 1000) + "\t开始尝试获取锁");
            try {
                if (lock.tryLock(2, TimeUnit.SECONDS)) {
                    System.out.println(Thread.currentThread() + "\t" + (System.currentTimeMillis() / 1000) + "\t获取锁成功");
                    TimeUnit.SECONDS.sleep(5);
                } else {
                    System.out.println(Thread.currentThread() + "\t" + (System.currentTimeMillis() / 1000) + "\t获取锁失败");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock.isHeldByCurrentThread() && lock.isLocked()) {
                    lock.unlock();
                    System.out.println(Thread.currentThread() + "\t" + (System.currentTimeMillis() / 1000) + "\t完成释放锁");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ClientThread(), "t1");
        Thread t2 = new Thread(new ClientThread(), "t2");
        Thread t3 = new Thread(new ClientThread(), "t3");
        t1.start();
        t2.start();
        t3.start();
        //t1.interrupt();

        TimeUnit.SECONDS.sleep(20);
    }
}

输出结果

Thread[t1,5,main]   1653540581  开始尝试获取锁
Thread[t3,5,main]   1653540581  开始尝试获取锁
Thread[t2,5,main]   1653540581  开始尝试获取锁
Thread[t1,5,main]   1653540581  获取锁成功
Thread[t3,5,main]   1653540583  获取锁失败
Thread[t2,5,main]   1653540583  获取锁失败
Thread[t1,5,main]   1653540586  完成释放锁

条件等待队列使用

Condition 是在 java 1.5 中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用 Condition,阻塞队列实际上是使用了 Condition 来模拟线程间协作。 Condition 是个接口,基本的方法就是 await() 和 signal() 方法; Condition 依赖于 Lock 接口,生成一个 Condition 的基本代码是 lock.newCondition() 调用 Condition 的 await() 和 signal() 方法,都必须在 lock 保护之内,就是说必须在 lock.lock() 和 lock.unlock 之间才可以使用:

  • Conditon中的await()对应Object的wait();
  • Condition中的signal()对应Object的notify();
  • Condition中的signalAll()对应Object的notifyAll()。

测试场景: 下面一个场景,需要ABC3个线程,A线程打印1次,然后是B线程打印2次,再是C线程打印3次,线程交替打印。 ABC线程需要交替执行,我们需要控制,线程的执行先后顺序 我们可以使用多条件Condition来控制,每一个线程拥有一个condition对象,调用各种的await方法,可以使线程等待,然后让别的线程调用这个condition对象的signal方法,唤醒线程。 代码如下:

public class ReentrantLockConditionTest {

    private int data = 1;
    private Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();


    public void printA() {
        lock.lock();
        try {
            while (data != 1) {
                condition1.await();
            }
            // 打印5次
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ->" + data);
            }
            data = 2;
            // 通知B线程
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            while (data != 2) {
                condition2.await();
            }
            // 打印10次
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ->" + data);
            }
            data = 3;
            // 通知C
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            while (data != 3) {
                condition3.await();
            }
            // 打印15次
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + " ->" + data);
            }
            data = 1;
            // 通知A
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockConditionTest conditionTest = new ReentrantLockConditionTest();
        // A,B,C 交替执行
        new Thread(conditionTest::printA, "A").start();
        new Thread(conditionTest::printB, "B").start();
        new Thread(conditionTest::printC, "C").start();
    }

}

输出结果如下:

A ->1
B ->2
B ->2
C ->3
C ->3
C ->3
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容