Java JUC ReentrantLock解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 独占锁 ReentrantLock 原理

独占锁 ReentrantLock 原理


介绍

ReentrantLock 是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞然后放入到该锁的AQS阻塞队列中。


它具有与synchronized相同的基本行为和语义,但 ReentrantLock 更灵活、更强大,增加了轮询、超时、中断等高级功能,并且还支持公平锁和非公平锁

image.png

从类图可以看到,ReentrantLock 还是使用 AQS 来实现的,并且可以根据参数来选择其内部是一个公平锁还是非公平锁,默认为非公平锁

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

Sync 类直接继承 AQS 类,它的子类 NonfairSync 和 FairSync 分别实现了获取锁的非公平和公平策略。

static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}

在 ReentrantLock 中 AQS 的 state 状态值表示线程获取锁的可重入次数,在默认情况下,state 值为 0 表示当前所没有任何线程持有。当一个线程第一次获取该锁后,会尝试使用 CAS 将 state 设置为 1,如果 CAS 成功则当前线程获取该锁,然后记录该锁的持有者为当前线程。在该线程第二次获取该锁后,则会将 state 设置为 2,这就是可重入次数。在该线程释放锁时,会尝试使用 CAS 将 state 减 1,如果减 1 后为 0 则释放锁。


获取锁

void lock()

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

当线程调用该方法时,如果当前锁没有被其他线程占用并且当前线程之前没获取过该锁,则当前线程获取,然后设置锁的拥有者为自己,随后设置 AQS 的 state 为 1,然后返回。


如果当前线程已经获取过该锁,则只是简单的将 AQS 的 state 加 1,然后返回。


如果该锁已经被其他线程所持有,则当前线程会进入 AQS 的阻塞队列中阻塞挂起。


在 ReentrantLock 中的 lock()方法委托给了 sync 类,sync 则根据 ReentrantLock 的构造函数选择使用 NonfairSync 或 FairSync。


我们先看看非公平锁的实现。

final void lock() {
    // CAS设置状态值
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 调用AQS的acquire方法
        acquire(1);
}

在 lock()中首先调用了 compareAndSetState 方法,因为默认 state 状态值为 0,所以第一个线程在首次调用该方法时通过 CAS 会设置为 1,随后成功获取到该锁,然后通过 setExclusiveOwnerThread 方法将锁持有者设置为当前线程。


当有其它线程通过 lock()来获取该锁时候,会 CAS 失败进入 else 调用 acquire(1)方法并且传参数为 1,下面我们再看一下 acquire 方法。

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

之前文章讲过,AQS 没有提供 tryAcquire 方法的实现,我们看一下 ReentrantLock 重写的 tryAcquire 方法,这里我们还是看非公平锁的实现。

static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
                //调用该方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
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;
}

该方法首先查看当前状态值是否为 0,为 0 则说明锁目前是空闲状态,然后尝试 CAS 获取锁,成功后 state 设置为 1,然后锁持有者为当前线程。


如果 state 不为 0,则说明锁已经被持有,如果持有者正好是当前线程则进行 state 加 1,然后返回 true,需要注意,如果 nextc < 0 则说明锁可能溢出。


如果当前线程不是持有者则返回 false 随后加入 AQS 阻塞队列。


下面我们看一下公平锁的实现。

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

公平锁和非公平锁的 tryAcquire 方法不同之处就是多了一个 hasQueuedPredecessors()方法,该方法就是实现公平锁的核心代码。

public final boolean hasQueuedPredecessors() {
    //读取头节点
    Node t = tail;
    //读取尾节点
    Node h = head;
    //s是首节点h的后继节点
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

在该方法中,因为队列是 FIFO 的,所以需要判断队列中有没有相关线程的节点已经在排队了。有则返回 true 表示线程需要排队,没有则返回 false 则表示线程无需排队。


首先我们看第一个条件h != t


  • 头节点和尾节点都为 null,表示队列都还是空的,甚至都没完成初始化,那么自然返回 fasle,无需排队。
  • 头节点和尾节点不为 null 但是相等,说明头节点和尾节点都指向一个元素,表示队列中只有一个节点,这时候也无需排队,因为队列中的第一个节点是不参与排队的,它持有着同步状态,那么第二个进来的节点就无需排队,因为它的前继节点就是头节点,所以第二个进来的节点就是第一个能正常获取同步状态的节点,第三个节点才需要排队,等待第二个节点释放同步状态。


接下来我们看第二个条件,(s = h.next) == null,如果h != t && s == null则说明有一个元素将要作为 AQS 的第一个节点入队,则返回 true。


接下来看第三个条件,s.thread != Thread.currentThread() ,判断后继节点是否为当前线程。


🙋🏻‍♀️ 举例,情况一:


h != t 返回 true,(s = h.next) == null 返回 false , s.thread != Thread.currentThread()返回 false


首先 h != t 返回true,说明队列中至少有两个不同节点存在;


(s = h.next) == null 返回false,说明头节点之后是有后继节点存在;


s.thread != Thread.currentThread()返回 false,说明当前线程和后继节点相同;


说明已经轮到当前节点去尝试获取同步状态,无需排队,返回 false


🙋🏻‍♀️ 举例,情况二:


h != t 返回 true,(s = h.next) == null 返回 true


首先 h != t 返回true,说明队列中至少有两个不同节点存在;


(s = h.next) == null 返回true,说明头节点也就是哨兵节点之后没有后继节点;


返回 true,说明需要排队


🙋🏻‍♀️ 举例,情况三:


h != t 返回 true,(s = h.next) == null 返回 false,s.thread != Thread.currentThread()返回 true


首先 h != t 返回true,说明队列中至少有两个不同节点存在;


(s = h.next) == null 返回false,说明头节点之后是有后继节点存在;


s.thread != Thread.currentThread()返回true,说明后继节点的线程不是当前线程,说明前面已经有人在排队了,还是得老老实实排队。


返回 true,说明需要排队

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        // 如果当前线程被中断,则抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取资源
        if (!tryAcquire(arg))
            //调用AQS可被中断的方法
            doAcquireInterruptibly(arg);
}

void lockInterruptibly()

该方法和 lock()方法类似,只不过它会对中断进行响应,就是当前线程在调用该方法时,如果他其他线程调用了当前线程的 interrupt()方法,则当前线程会抛出 InterruptedException 异常。


boolean tryLock()

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
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;
}

该方法尝试获取锁,如果当前锁没有被其他线程持有,则当前线程获取该锁并返回 true,否则返回 false。


📢 注意:该方法不会引起当前线程阻塞。


boolean tryLock(long timeout,TimeUnit unit)

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

与 tryLock 不同之处在于,设置了超时时间,如果超时时间到还没有获取到该锁,则返回 false。


释放锁

void unlock()

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) {
    int c = getState() - releases;
    //如果不是锁持有者调用unlock则抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果当前可重入次数为0 则情况锁持有线程
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

该方法首先尝试释放锁,如果 tryRelease()方法返回 true 则释放该锁,否则只是减 1,如果不是锁持有者去调用 unlock 则抛出 IllegalMonitorStateException 异常。


代码实践

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2022/1/21
 * @Description 使用ReentrantLock实现简单的线程安全List
 */
public class ReentrantLockList {
    private List<String> array = new ArrayList<>();
    private volatile ReentrantLock lock = new ReentrantLock();
    public void add(String e) {
        lock.lock();
        try {
            array.add(e);
        } finally {
            lock.unlock();
        }
    }
    public void remove(String e) {
        lock.lock();
        try {
            array.remove(e);
        } finally {
            lock.unlock();
        }
    }
    public String get(int index) {
        lock.lock();
        try {
            return array.get(index);
        } finally {
            lock.unlock();
        }
    }
}

该类在通过操作 array 之前,通过加锁来保证同一时间只有一个线程可以操作 array,但是也只能有一个线程可以对 array 元素进行访问。


总结

当同时有三个线程尝试获取独占锁 ReentrantLock 时,如果线程 1 获取到,则线程 2、3 都会被转换为 Node 节点随后被放入 ReentrantLock 对应的 AQS 阻塞队列中然后被挂起。

image.png

假设线程 1 在获取到锁之后,调用了锁创建的条件变量 1 进入 await 后,线程 1 就会释放该锁。然后线程 1 会被转换为 Node 节点插入条件变量 1 的条件队列中。


由于线程 1 释放了锁,所以阻塞队列中的线程 2,线程 3 都会有机会获取到该锁,假如使用的是公平锁,那么线程 2 则会获取到该锁,然后从 AQS 阻塞队列中移除线程 2 对应的 Node 节点。

image.png


相关文章
|
7天前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
55 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
14天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
12天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。
|
1月前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
88 2
|
3月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
89 0
|
3月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
69 0
|
3月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
75 0
|
13天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
13天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析