面试官: 有了解过ReentrantLock的底层实现吗?说说看

简介: 面试官: 有了解过ReentrantLock的底层实现吗?说说看

前言

目前正在出一个Java多线程专题长期系列教程,从入门到进阶含源码解读, 篇幅会较多, 喜欢的话,给个关注❤️ ~


源码剖析

上节带大家学习了它的基本使用,我们可以了解到它是一个可重入锁,下面我们就一起看一下它的底层实现~


构造函数

我们在使用的时候,都是先new它,所以我们先看下它的构造函数,它主要有两个:

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
复制代码

从字面上看,它们之间的不同点在于fair,翻译过来就是公平的意思,大体可以猜到它是用来构建公平锁和非公平锁,在继续往下看源码之前,先给大家科普一下这两种锁。


公平锁 & 非公平锁

  • 公平锁 多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。(例如银行办业务取号)


这种锁的优点很明显,每个线程都能够获取资源,缺点也很明显,如果某个线程阻塞了,其它线程也会阻塞,然而cpu唤醒开销很大,之前也给大家讲过


  • 非公平锁 多个线程都去尝试获取锁,获取不到就进入等待队列,cpu也不用去唤醒

优缺点正好和上边相反,优点减少开销,缺点也很明显,可能会导致一直获取不到锁或长时间获取不到锁

好,有了基本概念之后,我们继续往下看


NonfairSync

首先,我们看下非公平锁,默认情况下,我们申请的都是非公平锁,也就是new ReentrantLock(),我们接着看源码

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    /**
        * Performs lock.  Try immediate barge, backing up to normal
        * acquire on failure.
        */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
复制代码


它继承了Sync,Sync是一个内容静态抽象类:

abstract static class Sync extends AbstractQueuedSynchronizer {...}
复制代码


分为公平和非公平,使用AQS状态来表示持锁的次数,在构造函数初始化的时候都有sync = ...,我们接着看NonfairSync。在使用的时候,我们调用了lock.lock()方法,它是ReentrantLock的一个实例方法

// 获取锁
 public void lock() {
        sync.lock();
    }
复制代码


实际上内部还是调了sync的内部方法,因为我们申请的是非公平锁,所以我们看NonfairSync下的lock实现:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
复制代码


compareAndSetState这个方法,是AQS的内部方法,意思是如果当前状态值等于预期值,则自动将同步状态设置为给定的更新值。此操作具有volatile读写的内存语义。

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
复制代码


可以看到执行lock方法,会通过AQS机制计数,setExclusiveOwnerThread设置线程独占访问权限,它是AbstractOwnableSynchronizer的一个内部方法,子类通过使用它来管理线程独占

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {}
复制代码


可以看到它是继承了AbstractOwnableSynchronizer。下面接着看,我们说如果实际值等于期望值会执行上边的方法,不期望的时候会执行acquire(1)

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


这个方法以独占模式获取,忽略中断,它会尝试调用tryAcquire,成功会返回,不成功进入线程排队,可以重复阻塞和解除阻塞。看下AQS内部的这个方法

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
复制代码


我们可以看到实现肯定不在这,它的具体实现在NonfairSync

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
复制代码


可以看到它调用了,nonfairTryAcquire方法,这个方法是不公平的tryLock,具体实现在Sync内部,这里我们要重点关注一下

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 返回同步状态值,它是AQS内部的一个方法 
    //  private volatile int state;
    // protected final int getState() {
    //     return state;
    // }
    int c = getState();
    if (c == 0) {
        // 为0就比较一下,如果与期望值相同就设置为独占线程,说明锁已经拿到了
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 否则 判断如果当前线程已经是被设置独占线程了
    else if (current == getExclusiveOwnerThread()) {
        // 设置当前线程状态值 + 1 并返回成功
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 否则返回失败 没拿到锁
    return false;
}
复制代码


好,我们再回过头看下 acquire

public final void acquire(int arg) {
         // 如果当前线程没有获取到锁 并且 在队列中的线程尝试不断拿锁如果被打断了会返回true, 就会调用 selfInterrupt   
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
复制代码


selfInterrupt很好理解,线程中断

static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
复制代码


其实我们关注的重点是这个方法acquireQueued,首先关注一下入参,它内部传入了一个addWaiter,最后它回NODE节点

private Node addWaiter(Node mode) {
       // mode 没啥好说的就是一个标记,用于标记独占模式   static final Node EXCLUSIVE = null;
        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;
    }
复制代码


我们可以大体从猜到,Node是一个等待队列的节点类,是一个链表结构,之前我们讲FutureTask源码的时候也遇到过这种结构,它通常用于自旋锁,在这个地方,它是用于阻塞同步器

+------+  prev +-----+       +-----+
head |      | <---- |     | <---- |     |  tail
     +------+       +-----+       +-----+
复制代码


好,下面我们关注一下 acquireQueued

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        // 默认是 false
        boolean interrupted = false;
        // 进入阻塞循环遍历 线程队列
        for (;;) {
            // 返回前一个节点
            final Node p = node.predecessor();
            // 判断如果前一个节点是头部节点,并且拿到锁了,就会设置当前节点为头部节点
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                // 这里可以看到注释 help gc ,
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 检查并更新未能获取的节点的状态。如果线程应该阻塞,则返回 true 并且线程中断了
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果失败  取消正在尝试获取的节点
        if (failed)
            cancelAcquire(node);
    }
}
复制代码


从上面的源码来看,在体会一下上面讲的非公平锁的概念,是不是更好理解一些,然后就是释放锁unlock,这个方法我们可以看到是ReentrantLock下的一个实例方法,所以公平锁的释放锁也是调的这个方法,其实最终可以猜到调用的还是sync的方法

public void unlock() {
        sync.release(1);
    }
复制代码


Sync继承AQS,release是AQS的内部方法

public final boolean release(int arg) {
       // 尝试释放锁  tryRelease 在Sync内部
        if (tryRelease(arg)) {
            Node h = head;
            // 如果节点存在 并且状态值不为0
            if (h != null && h.waitStatus != 0)
                // 唤醒下个节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
复制代码


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;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 可以看到调用了 LockSupport来唤醒
            LockSupport.unpark(s.thread);
    }
复制代码


我们再看下tryRelease, 同样这个实现在Sync

protected final boolean tryRelease(int releases) {
            // 同样释放锁的时候 依然使用 AQS计数
            int c = getState() - releases;
            // 判断当前线程是否是独占线程,不是抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果是0 表示是释放成功
            if (c == 0) {
                free = true;
                // 并且把独占线程设为null
                setExclusiveOwnerThread(null);
            }
            // 更新状态值
            setState(c);
            return free;
        }
复制代码


FairSync

公平锁FairSync的区别在于,它的获取锁的实现在它的内部,Sync默认内部实现了非公平锁

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        // 这个方法最终调用 tryAcquire
        final void lock() {
            acquire(1);
        }
        // 公平锁的实现
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 这边和非公平锁的实现有些相似 同样判断状态
            if (c == 0) {
                // 判断排队队列是否存在, 不存在并且比较期望值
                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;
        }
    }
复制代码


它的实现比较简单,通过实现可以发现,它按照申请锁的顺序来获取锁,排第一的先拿到锁,在结合上面的概念理解一下,就很好理解了.


释放锁unlock,上面我们已经讲过了~


结束语

本节内容可能有点多,主要是看源码,可以打断点自己调一下, 举一反三,通过源码去理解一下什么是公平锁和非公平锁, ReentrantLock可重入锁体验在哪里。下一节,带大家理解一下CAS与原子操作的概念 ~

相关文章
|
5月前
|
Java
【面试问题】Synchronized 和 ReentrantLock 区别?
【1月更文挑战第27天】【面试问题】Synchronized 和 ReentrantLock 区别?
|
5月前
|
存储 安全 Java
《吊打面试官》从根上剖析ReentrantLock的来龙去脉
《吊打面试官》从根上剖析ReentrantLock的来龙去脉
|
存储 Java
「Java面试」工作3年竟然回答不出如何理解Reentrantlock实现原理
一个3 年工作经验的小伙伴,在面试的时候被这样一个问题。”谈谈你对ReentrantLock实现原理的理解“,他当时零零散散的说了一些。但好像没有说关键点。希望我分享一下我的理解。
90 0
|
2月前
|
Java
【多线程面试题十六】、谈谈ReentrantLock的实现原理
这篇文章解释了`ReentrantLock`的实现原理,它基于Java中的`AbstractQueuedSynchronizer`(AQS)构建,通过重写AQS的`tryAcquire`和`tryRelease`方法来实现锁的获取与释放,并详细描述了AQS内部的同步队列和条件队列以及独占模式的工作原理。
【多线程面试题十六】、谈谈ReentrantLock的实现原理
|
5月前
|
Java API 调度
大厂高频面试题:ReentrantLock 与 synchronized异同点对比
【5月更文挑战第7天】大厂高频面试题:ReentrantLock 与 synchronized异同点对比
61 0
Java 最常见的面试题:synchronized 和 ReentrantLock 区别是什么?
Java 最常见的面试题:synchronized 和 ReentrantLock 区别是什么?
|
安全 Java 开发者
京东面试官:说说synchronized 和 ReentrantLock的区别!
前面我们介绍了很多关于多线程的内容,在多线程中有一个很重要的课题需要我们攻克,那就是线程安全问题。线程安全问题指的是在多线程中,各线程之间因为同时操作所产生的数据污染或其他非预期的程序运行结果。
126 0
|
安全 调度
面试:为了进阿里,需要深入理解ReentrantLock原理
在面试,很多时间面试官都会问到锁的问题,ReentrantLock也是常问一个点
137 0
面试:为了进阿里,需要深入理解ReentrantLock原理
|
Java 程序员
面试被问AQS、ReentrantLock答不出来?这些知识点让我和面试官聊了半小时!
Java并发编程的核心在于java.concurrent.util包,juc中大多数同步器的实现都围绕了一个公共的行为,比如等待队列、条件队列、独占获取、共享获取等,这个行为的抽象就是基于AbstractQueuedSynchronized(AQS)。AQS定义了多线程访问共享资源的同步器框架。
教面试官ReentrantLock源码(下)
教面试官ReentrantLock源码(下)
111 0
教面试官ReentrantLock源码(下)