首页 > 编程语言 >Java Reentrantlock可重入锁原理 | 源码探究

Java Reentrantlock可重入锁原理 | 源码探究

时间:2024-08-12 15:53:23浏览次数:17  
标签:Java int lock Reentrantlock 获取 state 源码 线程 return

一、基本概念

ReentrantLock 是 Java 中提供的一个可重入互斥锁,它是 java.util.concurrent.locks 包中的一个接口 Lock 的实现类。ReentrantLock 提供了比使用 synchronized 关键字更强大的锁定机制,例如 公平锁 和 非公平锁 选择、尝试锁定、可中断锁定等。

ReentrantLock 主要有以下几个特点:

  • 可重入性:允许同一个线程多次获取同一个锁。
  • 公平性:可以选择是否按照请求锁的顺序来获取锁。
  • 扩展性:提供了多种锁操作,如 tryLock 和 unlock 方法。
  • 中断响应:支持响应中断信号,使线程可以在等待锁时响应中断。

可重入性

可重入性是指一个线程可以多次获取同一个锁的能力。在 Java 中,synchronized 关键字也支持可重入性,但 ReentrantLock 提供了更灵活的方式来实现这一特性。

在 ReentrantLock 的内部实现中,可重入性是通过 AbstractQueuedSynchronizer(AQS)中的 state 字段来跟踪的。state 字段是一个 volatile 的整型变量,用于记录锁的持有状态。

/**
 * The synchronization state.
 */
private volatile int state;
/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a {@code volatile} read.
 * @return current state value
 */
protected final int getState() {
    return state;
}
/**
 * Sets the value of synchronization state.
 * This operation has memory semantics of a {@code volatile} write.
 * @param newState the new state value
 */
protected final void setState(int newState) {
    state = newState;
}
/**
 * Atomically sets synchronization state to the given updated
 * value if the current state value equals the expected value.
 * This operation has memory semantics of a {@code volatile} read
 * and write.
 *
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful. False return indicates that the actual
 *         value was not equal to the expected value.
 */
// 以CAS原子方式更新state值
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
  • 获取锁:当线程尝试获取锁时,如果该线程已经拥有锁,那么 state 的值会增加。这表示该线程可以再次获取锁。
  • 释放锁:当线程释放锁时,state 的值会减少。只有当 state 的值变为零时,锁才会被完全释放,并允许其他线程获取锁。

举个例子:

抓娃娃机,玩家需要投入游戏币才能使用。每次投入一枚游戏币,玩家就可以尝试抓取一次娃娃。现在,我们将这台抓娃娃机类比为一个可重入锁(ReentrantLock),其中娃娃机的当前投币数可以被视为锁的状态计数器(state)。

可重入锁的状态(state

  • 当 state 为 0 时,表示当前娃娃机没有人在使用(锁未被任何线程持有)。
  • 当 state 大于 0 时,表示娃娃机正在被某位玩家使用,并且 state 的值表示该玩家投入的游戏币数量(即该玩家已经获取锁的次数)。

玩家使用娃娃机的过程

  • 小明投币使用:小明可以一次性投入多枚游戏币,这意味着他可以连续抓取多次娃娃(可重入性,一个线程可以多次获取同一把锁)。
  • 抓取娃娃:每当小明抓取一次娃娃,state 的值减少 1,表示小明使用了一枚游戏币。
  • 抓取结束:只有当 state 的值回到 0 时,表示小明已经使用完了所有投入的游戏币,这时娃娃机才可供下一位玩家使用(锁被完全释放,允许其他线程获取锁)。 state 不为0时,其他玩家应当等待小明玩完才能够使用(阻塞)

公平锁

公平锁遵循先进先出的原则,即按照线程请求锁的顺序来获取锁。这意味着如果一个线程在另一个线程之前请求了锁,那么它将在那个线程之前获取到锁。

公平锁的实现主要是通过 AbstractQueuedSynchronizer 中的 hasQueuedPredecessors() 方法来判断是否有其他线程排在当前线程前面。如果存在这样的线程,那么当前线程就会等待,直到前面的线程释放锁。

举个例子:

还是抓娃娃,当 state 不为0时,即小明正在使用这台娃娃机,其他玩家依次在后面排队等待,当小明用完所有的币时,队首的人可以使用这台娃娃机。

Reentrantlock FairSync类中的实现:

@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 获取同步器的内部状态值。
    int c = getState();
    if (c == 0) {
        // 检查是否有其他线程在队列中等待
        if (!hasQueuedPredecessors() && // 确保当前线程是第一个等待的线程
                compareAndSetState(0, acquires)) { // 尝试设置 state 为 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;
}
  • int c = getState();
    AbstractQueuedSynchronizer 类继承的方法,用于获取同步器的内部状态值。该值用来判断锁是否已经被其他线程持有
  • // 检查是否有其他线程在队列中等待
    if (!hasQueuedPredecessors() && // 确保当前线程是第一个等待的线程
            compareAndSetState(0, acquires)) { // 尝试设置 state 为 acquires
        setExclusiveOwnerThread(current);
        return true;
    }

!hasQueuedPredecessors(): 检查当前线程前面是否有等待获取锁的线程。公平锁要求所有线程排队等待获取锁,所以只有排在队首的线程才能尝试获取锁;

compareAndSetState(0, acquires): 尝试将锁的状态从0设置为acquires;

setExclusiveOwnerThread(current): 如果当前线程前面没有等待获取锁的进程,并且锁状态值为0,则设置当前线程为锁的独占所有者。

  • // 重入锁处理
    else if (current == getExclusiveOwnerThread()) {
        // 更新锁状态值
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }

    如果当前线程是锁的持有者,则根据重入值更新state。

非公平锁

非公平锁不保证线程获取锁的顺序。在非公平模式下,即使有其他线程正在等待锁,当前线程仍然可以直接尝试获取锁。这种模式可能会导致饥饿现象,但通常具有更好的吞吐量。

非公平锁的实现中,当前线程在尝试获取锁时不会检查是否有其他线程在等待队列中。这意味着当前线程总是会尝试获取锁,无论是否有其他线程正在等待。

举个例子:

其他玩家不会排队等待这台娃娃机,而是当小明用完所有的币时,所有玩家都会尝试来使用这台娃娃机,每个人都有机会进行使用,而最先抢到的拥有使用权。

Reentrantlock NonFairSync类中的实现:

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

与公平锁类似,差别在于非公平锁不会判断当前线程前是否有其他线程在尝试获取锁,而是一旦compareAndSetState(0, acquires)成功,就会设置当前线程为锁的持有者。

二、ReentrantLock执行流程

递归计算斐波那契数列:

private final ReentrantLock lock = new ReentrantLock();
public long fibonacci(long n) {
    lock.lock();
    try {
        if (n <= 1) {
            return n;
        } else {
            // 递归调用时,由于同一个线程持有锁,不会发生阻塞
            return fibonacci(n - 1) + fibonacci(n - 2);
        }
    } finally {
        lock.unlock();
    }
}

首先看构造函数,ReentrantLock默认使用非公平锁:

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync(); // 默认非公平锁
}
/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync(); // 可以传入boolean参数决定使用哪种锁
}

加锁 lock.lock()

执行lock.lock();方法时,尝试加锁:

public void lock() {
    sync.lock();
}
  • 公平锁

公平锁会尝试以排队方式(见上述公平锁tryAcquire方法)获取锁,如果获取失败,则将线程加入到等待队列中,接下来调用 acquireQueued 方法,该方法会阻塞当前线程,直到锁被释放,并有机会再次尝试获取锁。

// lock
final void lock() {
    acquire(1);
}

// acquire
@ReservedStackAccess
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • 非公平锁

非公平锁则是直接尝试获取锁,获取失败同样将线程加入到等待队列中并阻塞。

@ReservedStackAccess
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

解锁 lock.unlock()

// unlock入口方法 
public void unlock() {
    sync.release(1);
}

// release
@ReservedStackAccess
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease(int arg): 

  • 这是一个内部方法,用于尝试释放锁。
  • 它会减少 state 的值,表示锁的持有计数减一。
  • 如果 state 的值变为零,那么锁被完全释放,并且当前线程不再是锁的所有者。
  • 如果成功释放锁,返回 true;否则返回 false。

Node h = head:

  • 获取等待队列的头部节点。头部节点是当前持有锁的线程对应的节点,或者是在锁被释放后准备尝试获取锁的线程对应的节点。

if (h != null && h.waitStatus != 0):

  • 检查头部节点是否存在,以及它的 waitStatus 是否不为零。
  • waitStatus 表示节点的状态,通常用来指示线程是否被阻塞。
  • 如果 waitStatus 不为零,那么它可能处于阻塞状态。
  • 如果头部节点存在并且它的 waitStatus 不为零,那么接下来将尝试唤醒它的后继节点。

unparkSuccessor(Node node):

  • 唤醒头部节点的后继节点。

标签:Java,int,lock,Reentrantlock,获取,state,源码,线程,return
From: https://blog.csdn.net/m0_62467665/article/details/141057015

相关文章

  • [Java/SQL] 自动去除SQL注释
    0引言在数据连接(池)框架、数据服务/DatatoAPI等中间平台中,因部分数据库不允许SQL中存在注释信息(如:Redis、OpenGemini等;当然,MYSQL、INFLUXDB等数据库是支持的),故存在这样一个需求:在提交到数据库的SQL,需对原始SQL的注释信息予以去除。以OpenGeminiV1.2.0数据库为例,如果......
  • Java基础-学习笔记08
    01类变量、类方法、main方法、代码块类变量(静态变量)类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。关于静态变量在内存中的存放地址,有两种说法,①认为静态变量......
  • JDK自带命令:深入理解Java程序的运行机制
    JDK(JavaDevelopmentKit)是Java开发和运行环境的核心,它提供了丰富的命令和工具来帮助我们更好地理解和控制Java程序的运行。本文将详细介绍JDK自带的一些关键命令,以及它们的详细参数和执行结果。1.jps(JavaVirtualMachineProcessStatusTool)jps命令用于列出正在运行的......
  • JSP广州中小学学校信息管理系统_j3o8r(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表系统功能:用户,区域分类,招生信息,机构活动,成绩排名,获奖排名,社团排名,特色排名开题报告内容一、项目背景与意义随着教育信息化步伐的加快,广州作为教育强市,对......
  • JAVA中的File类
    File类概述`File`类提供了一系列的方法来操作文件和目录。它不直接访问文件内容本身,而是操作文件系统的属性,如文件大小、最后修改时间、文件类型(普通文件、目录等)等。需要注意的是,`File`对象本身只是文件路径的抽象表示,只有当通过`File`对象调用相关方法时,才会与底层文件系统......
  • 三十分钟入门基础Go(Java小子版)
    前言Go语言定义Go(又称Golang)是Google的RobertGriesemer,RobPike及KenThompson开发的一种静态、强类型、编译型语言。Go语言语法与C相近,但功能上有:内存安全,GC,结构形态及CSP-style并发计算。适用范围本篇文章适用于学习过其他面向对象语言(Java、Php),但没有学过......
  • 科普文:Java基础系列之【你必须知道的框架基础-代理详解】
     概叙科普文:Java基础系列之【你必须知道的框架基础-反射/代理】-CSDN博客前面我们详细讲解了反射,反射作用在类加载后创建对象这个期间,再来看看代理。反射是基础,通过反射获取对象及其属性和操作;代理则可以将反射出来的类包装成目标类,然后构建一个代理类,通过代理类来操控目......
  • 科普文:Java基础系列之【你必须知道的框架基础-反射/代理】
    前言科普文:Java基础系列之【Java动态代理的应用场景和基本原理】-CSDN博客科普文:Java基础系列之【字节码增强技术探索】-CSDN博客科普文:Java基础系列之【字节码应用案例Fastjson原理和实操说明】-CSDN博客科普文:Java基础系列之【JVM字节码操作ASM框架概叙】-CSDN博客......
  • Rounding necessary错误解决Java的BigDecimal除法的
    出现Roundingnecessary错误原因是使用了BigDecimal的setScale方法导致。错误原因:setScale方法保留小数位数小于实际位数并且未指定roundingMode参数即报错。如下代码:BigDecimalrs=newBigDecimal("2057.9200");rs.setScale(2);上述代码实际数值是2057.9200是4位小......
  • 最新Java面试题及答案(500道)
    第一章-Java基础篇Object中有哪些方法   难度系数:⭐protectedObjectclone()--->创建并返回此对象的一个副本。booleanequals(Objectobj)--->指示某个其他对象是否与此对象“相等protectedvoidfinalize()--->当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃......