操作系统锁

操作系统锁

Linux 操作系统 为核心(服务器端高并发场景的主流 OS),从本质定义、底层基石、锁类型全解析、生命周期与开销、工程问题与避坑、语言层映射6 个维度,对操作系统锁做完整、深入的展开,覆盖从内核实现到业务开发的全链路细节。

一、操作系统锁的核心定义与本质边界

1. 核心定义

操作系统锁(内核级锁) :由操作系统内核提供、依赖内核调度器实现线程同步 的底层原语。其核心特征是:当线程竞争锁失败时,会从用户态陷入内核态 ,被内核调度器移出 CPU 运行队列,放入该锁专属的内核等待队列进入阻塞休眠;当锁被释放后,内核再唤醒等待队列中的线程,重新竞争锁并切回用户态执行。

2. 与用户态锁的本质区别

这是理解操作系统锁的核心前提,二者的性能、开销、适用场景完全不同:

维度 操作系统锁(内核级) 用户态锁(自旋锁 / CAS / 偏向锁)
执行权限 竞争时陷入内核态,全程依赖内核调度 全程用户态执行,不涉及内核切换
等待策略 竞争失败放弃 CPU,线程阻塞休眠 竞争失败不放弃 CPU,原地自旋重试
核心开销 系统调用、用户 / 内核态上下文切换、线程调度 CPU 空转、缓存行失效
适用场景 临界区较长、竞争不极端的场景 临界区极短、CPU 核心充足的场景

3. 现代操作系统锁的核心设计思想

传统的纯内核锁(每次加解锁都要系统调用)性能极差,现代 Linux 的操作系统锁均基于Futex(快速用户态互斥锁) 实现,采用 \\ 「无竞争时全用户态,有竞争时才陷内核」\\ 的混合设计,在保证通用性的同时,最大化降低了无竞争场景的开销。

二、Linux 操作系统锁的底层基石:Futex

Futex(Fast Userspace Mutex)是 Linux 内核 2.6 版本后引入的底层同步原语,是 Linux 下所有用户态操作系统锁(pthread_mutex、rwlock、条件变量等)的底层实现核心,理解 Futex 是理解所有 Linux 操作系统锁的关键。

1. Futex 的核心原理

Futex 由两部分组成:

  • 用户态的 32 位整型变量(futex word):用于标记锁的状态,线程先在用户态通过 CPU 原子指令(CAS)操作该变量,尝试获取锁;

  • 内核态的等待队列:内核为每个唯一的 futex word 内存地址维护一个等待队列,仅当锁竞争发生时,线程才通过系统调用陷入内核,进入等待队列阻塞。

核心设计逻辑:绝大多数场景下锁是无竞争的,完全在用户态完成加解锁,零系统调用开销;仅当真正发生竞争时,才进入内核做阻塞 / 唤醒调度,完美平衡了通用性和性能。

2. Futex 的核心系统调用与操作

Futex 的核心能力由sys\_futex系统调用提供,核心操作只有两个,其他均为扩展能力:

c 复制代码
#include <linux/futex.h>
#include <sys/syscall.h>
// 核心系统调用原型
int syscall(SYS_futex, uint32_t *uaddr, int futex_op, uint32_t val,
            const struct timespec *timeout, uint32_t *uaddr2, uint32_t val3);
核心操作 功能说明 执行逻辑
FUTEX\_WAIT 线程阻塞等待 内核先原子检查\*uaddr是否等于预期值val: 1. 相等:将当前线程放入该 futex 的等待队列,阻塞休眠; 2. 不相等:立即返回,不阻塞,由用户态重试
FUTEX\_WAKE 唤醒等待的线程 内核唤醒uaddr对应等待队列中最多val个线程(通常传 1,仅唤醒一个线程避免惊群)

3. Futex 的完整执行流程(以互斥锁为例)

  1. 加锁(无竞争):线程在用户态通过 CAS 指令,将 futex word 从 0(空闲)改为 1(已持有),成功则直接返回,全程无系统调用;

  2. 加锁(有竞争) :CAS 失败,说明锁已被持有,线程将 futex word 改为 2(有线程等待),调用FUTEX\_WAIT陷入内核,阻塞休眠;

  3. 解锁(无等待):线程将 futex word 从 1 改回 0,无等待线程,直接返回,无系统调用;

  4. 解锁(有等待) :线程发现 futex word 为 2,说明有线程阻塞,调用FUTEX\_WAKE陷入内核,唤醒等待队列中的一个线程。

三、主流操作系统锁类型 全量深度解析

基于 Futex 这个底层基石,Linux 封装了一系列面向业务开发的操作系统锁,下面逐个展开其核心原理、实现细节、适用场景与工程避坑。

1. 互斥锁(Mutex):最基础、最核心的操作系统锁

互斥锁是操作系统提供的最基础的同步原语,也是业务开发中最常用的锁,核心语义是排他性互斥:同一时刻,只有一个线程能持有锁,进入临界区;其他线程竞争锁失败时,会进入内核阻塞等待。

1.1 底层实现与数据结构

Linux 下的互斥锁由pthread库封装,类型为pthread\_mutex\_t,其底层核心结构如下(glibc 实现):

c 复制代码
typedef union {
  struct __pthread_mutex_s {
    int __lock;           // futex word,锁状态:0=空闲,1=已持有,2=有线程等待
    unsigned int __count; // 递归锁计数,记录同一线程的重入次数
    int __owner;          // 锁持有者的线程ID
    int __kind;           // 锁类型:普通、递归、检错、自适应
    int __spins;          // 自适应自旋次数
    __pthread_list_t __list; // 等待队列节点
  } __data;
  char __size[32]; // 固定大小,保证二进制兼容
  long int __align; // 内存对齐
} pthread_mutex_t;

整个互斥锁的加解锁逻辑,完全基于上文中的 Futex 机制实现,无竞争时全用户态,竞争时才陷内核。

1.2 互斥锁的 4 种关键类型

通过\_\_kind字段,Linux 支持 4 种不同类型的互斥锁,适配不同场景:

锁类型 核心特性 适用场景 注意事项
PTHREAD\_MUTEX\_NORMAL(普通锁,默认) 标准互斥语义,不支持重入,重复加锁会死锁;解锁不校验持有者,其他线程可释放 绝大多数通用场景 禁止重复加锁、跨线程释放,否则死锁 / 未定义行为
PTHREAD\_MUTEX\_RECURSIVE(递归锁 / 可重入锁) 支持同一线程多次加锁,\_\_count记录重入次数,解锁次数需与加锁次数匹配才会真正释放 嵌套加锁、递归函数加锁场景 会增加锁开销,不可滥用,避免死锁
PTHREAD\_MUTEX\_ERRORCHECK(检错锁) 加锁 / 解锁做完整校验:重复加锁返回错误、非持有者解锁返回错误 调试阶段、开发自测 校验会带来额外开销,生产环境慎用
PTHREAD\_MUTEX\_ADAPTIVE\_NP(自适应锁) 竞争失败时先自旋一段时间,自旋失败再进入内核阻塞,结合了自旋锁和阻塞锁的优势 临界区极短、竞争不极端的高吞吐场景 单核 CPU 无效,长临界区自旋会浪费 CPU
1.3 核心扩展能力:优先级继承

互斥锁支持优先级继承协议 ,用于解决实时系统中经典的优先级反转问题(后文详细展开)。当高优先级线程等待低优先级线程持有的锁时,内核会临时将低优先级线程的优先级提升至高优先级线程的级别,保证其快速执行并释放锁,避免高优先级线程长期阻塞。

1.4 适用场景
  • 绝大多数通用的临界区互斥场景;

  • 临界区执行时间较长(超过 1000 条 CPU 指令),不适合自旋的场景;

  • 需要线程阻塞等待,不希望空耗 CPU 的场景。


2. 读写锁(RWLock):读多写少场景的专属优化锁

读写锁是对互斥锁的场景化优化,核心解决读多写少场景下,互斥锁导致读操作完全串行化的性能瓶颈

2.1 核心原理与语义

读写锁将锁分为两种模式,核心语义是读共享、写排他

  • 读锁(共享锁):多个线程可同时持有读锁,读 - 读之间完全不互斥,并发度拉满;

  • 写锁(排他锁):同一时刻只能有一个线程持有写锁,写锁与所有读锁、其他写锁完全互斥。

2.2 底层实现

Linux 下的读写锁由pthread\_rwlock\_t实现,底层同样基于 Futex 机制,内核维护两个等待队列(读等待队列、写等待队列),并通过计数器记录当前持有读锁的线程数。

核心加锁逻辑:

  1. 加读锁:无写锁持有、无写线程等待(写优先模式),则读计数器 + 1,加锁成功;否则进入读等待队列阻塞;

  2. 加写锁:无读锁持有、无写锁持有,加锁成功;否则进入写等待队列阻塞;

  3. 解锁:读锁释放则计数器 - 1,计数器归 0 则唤醒写等待线程;写锁释放则根据调度策略,唤醒读线程或写线程。

2.3 两种核心调度策略

读写锁的核心痛点是写饥饿,因此 Linux 提供了两种调度策略:

  • 读优先模式:只要有读锁持有,后续的读线程都能直接加锁,写线程必须等待所有读锁释放。优点是读并发度高,缺点是写线程可能长期饥饿;

  • 写优先模式:只要有写线程在等待,后续的读线程都会被阻塞,必须等待写线程加锁并释放后,才能加读锁。优点是避免写饥饿,缺点是读并发度会下降。

2.4 工程避坑
  • 锁升级死锁:线程持有读锁时,尝试加写锁,会导致永久死锁(写锁需要等待所有读锁释放,包括自己的读锁);

  • 写饥饿:读优先模式下,高频读操作会导致写线程长期无法获取锁;

  • 开销高于互斥锁:读写锁的维护逻辑更复杂,读少写多场景下,性能反而不如普通互斥锁。

2.5 适用场景

读操作频率远高于写操作(读:写 &gt; 10:1)、读操作耗时较长的场景,如配置管理、元数据缓存、路由表查询等。


3. 条件变量(Condition Variable):等待 - 唤醒专属同步原语

条件变量不是锁 ,是配合互斥锁使用的线程等待 - 唤醒原语,核心解决「线程需要等待某个条件满足才能执行,轮询等待会空耗 CPU」的问题。

3.1 核心原理

条件变量的核心逻辑是:线程持有互斥锁时,发现业务条件不满足,就原子性地释放互斥锁 + 进入内核阻塞等待 ;当其他线程修改了业务条件,通过条件变量唤醒等待线程后,被唤醒的线程会重新竞争互斥锁,再检查条件是否满足。

必须配合互斥锁的核心原因:

  • 保护「业务条件」这个共享资源,避免多线程并发修改导致的竞态;

  • 保证「释放锁」和「进入阻塞」是原子操作,避免唤醒信号丢失。

3.2 底层实现与核心操作

Linux 下的条件变量由pthread\_cond\_t实现,底层基于 Futex 机制,内核维护一个等待队列,核心操作有 4 个:

核心操作 功能说明
pthread\_cond\_wait 原子释放互斥锁,线程进入阻塞等待,被唤醒后重新竞争互斥锁
pthread\_cond\_timedwait 带超时的 wait,超时后自动唤醒返回,避免永久阻塞
pthread\_cond\_signal 唤醒等待队列中的一个线程,避免惊群
pthread\_cond\_broadcast 唤醒等待队列中的所有线程
3.3 两个核心工程问题
(1)虚假唤醒

线程被signal/broadcast唤醒后,业务条件可能仍然不满足,这就是虚假唤醒。

  • 发生原因:内核可能会产生无理由的唤醒、多线程竞争导致条件被其他线程抢先修改;

  • 解决方案:必须用 while 循环包裹条件检查,而非 if 判断 ,唤醒后再次检查条件,不满足则继续 wait。

    正确示例:

    c 复制代码
    pthread_mutex_lock(&mutex);
    // 必须用while,不能用if
    while (条件不满足) {
      pthread_cond_wait(&cond, &mutex);
    }
    // 执行业务逻辑
    pthread_mutex_unlock(&mutex);
(2)惊群问题

调用pthread\_cond\_broadcast时,会唤醒所有等待线程,但最终只有一个线程能抢到互斥锁,其他线程抢到锁后发现条件不满足,又会重新进入阻塞,这就是惊群问题。

  • 危害:大量线程被无效唤醒,导致频繁的内核态切换、CPU 飙升、性能下降;

  • 解决方案:仅当所有等待线程都需要被唤醒时才用 broadcast,绝大多数场景用 signal 仅唤醒一个线程即可。

3.4 适用场景

生产者 - 消费者模型、任务队列、线程池、状态机等待等「线程需要等待特定条件触发」的场景。


4. 信号量(Semaphore):计数式同步原语

信号量是操作系统提供的计数型同步原语,核心用于控制同时访问共享资源的线程数量,也可实现互斥锁、线程同步栅栏等功能。

4.1 核心原理

信号量内部维护一个非负整型计数器,核心操作是两个原子操作:

  • P 操作(申请 / 减 1):若计数器 &gt; 0,则计数器 - 1,直接返回;若计数器 = 0,则线程进入内核阻塞等待,直到计数器 &gt; 0;

  • V 操作(释放 / 加 1):计数器 + 1,若有线程在等待,则唤醒其中一个线程。

根据计数器的初始值,信号量分为两类:

  • 二元信号量:计数器初始值 = 1,P/V 操作对应加锁 / 解锁,退化为互斥锁;

  • 计数信号量:计数器初始值 = N&gt;1,允许最多 N 个线程同时进入临界区,实现并发限流。

4.2 底层实现

Linux 提供两种 POSIX 信号量,底层均基于 Futex 机制实现:

  • 无名信号量:用于同一进程内的多线程同步,存储在进程内存中;

  • 有名信号量:用于跨进程的同步,存储在文件系统中,不同进程可通过同一个文件路径访问。

4.3 与互斥锁的核心区别
维度 信号量 互斥锁
所有权 无所有权概念,任何线程都可以执行 V 操作释放 有严格所有权,只有锁持有者才能释放
并发控制 支持 N 个线程同时进入临界区 仅支持 1 个线程进入临界区
功能范围 可实现互斥、限流、线程同步、栅栏等 仅用于临界区互斥
死锁风险 无重入机制,重复 P 操作会死锁 递归锁支持重入,可避免嵌套死锁
4.4 适用场景
  • 资源池限流:如数据库连接池、线程池、网络连接数控制;

  • 跨进程同步:多进程间的临界区互斥;

  • 生产者 - 消费者模型:用两个信号量分别控制空缓冲区和满缓冲区数量。


5. 内核态专属锁

上述锁均为用户态封装的操作系统锁,Linux 内核本身还提供了一系列专属的同步原语,用于内核态开发(驱动、内核模块、系统调用实现),核心区别是:内核态锁运行在内核上下文,不允许随意阻塞、调度,对执行时长有严格限制。

5.1 内核自旋锁(Spinlock)
  • 核心原理:竞争锁失败时,线程在内核态原地自旋忙等,不放弃 CPU,不进入阻塞,直到抢到锁;

  • 核心约束:临界区必须极短,持有锁期间不能睡眠、不能触发调度、不能阻塞;

  • 适用场景:中断上下文、内核短临界区、多核 CPU 内核态同步,是内核中最基础的锁。

5.2 顺序锁(Seqlock)
  • 核心原理:基于一个递增的序列号,写操作时序列号 + 1,读操作前后两次读取序列号,若序列号一致且为偶数,说明读数据有效;

  • 核心优势:读操作完全无锁,写操作不会被读操作阻塞,读写完全不互斥;

  • 适用场景:读极多写极少、数据量小的场景,如内核的系统时间、网络路由表更新。

5.3 内核互斥锁(struct mutex)
  • 核心原理:用户态互斥锁的内核版本,竞争失败时内核线程会进入阻塞休眠,放弃 CPU;

  • 适用场景:内核态临界区较长、允许睡眠的场景,相比自旋锁更节省 CPU。

5.4 RCU(Read-Copy-Update)
  • 核心原理:读操作完全无锁,零开销;写操作时复制数据副本,修改完成后等待所有访问旧数据的读者退出临界区,再原子替换指针;

  • 核心优势:读操作性能极致,无任何开销,适合高频读场景;

  • 适用场景:Linux 内核中大规模使用,如网络协议栈、文件系统、设备驱动的高频读场景。


6. 其他操作系统同步原语

  • 文件锁:用于跨进程的文件访问互斥,分为建议锁(flock)和强制锁,Linux 下底层由 VFS 实现;

  • 屏障(Barrier):用于批量线程同步,所有线程到达屏障点后全部阻塞,直到预定数量的线程都到达,才统一放行,适用于并行计算的多线程分片同步;

  • 大内核锁(BKL):Linux 早期的全局内核锁,已被废弃,被更细粒度的锁替代。

四、操作系统锁的完整生命周期与开销拆解

1. 互斥锁的完整加锁生命周期

  1. 用户态原子尝试 :线程调用pthread\_mutex\_lock,先在用户态通过 CAS 指令修改 futex word,尝试加锁;

  2. 无竞争快速路径:CAS 成功,加锁完成,全程用户态,无系统调用,耗时约 10ns 级别;

  3. 有竞争慢速路径 :CAS 失败,说明锁已被持有,先执行自适应自旋(自适应锁),自旋失败后,调用FUTEX\_WAIT系统调用;

  4. 内核态陷入:CPU 从用户态切换到内核态,保存用户态上下文,切换内核栈,耗时约 100-200ns;

  5. 内核检查与阻塞:内核检查锁状态,确认仍被持有,将当前线程状态置为 TASK_INTERRUPTIBLE,加入锁的等待队列,触发调度器切换;

  6. 线程调度切换:调度器将当前线程移出 CPU 运行队列,切换其他就绪线程执行,上下文切换耗时约 500ns-1μs;

  7. 唤醒与重试 :锁持有者释放锁时,调用FUTEX\_WAKE系统调用,内核唤醒等待队列中的线程,将其重新加入运行队列,等待 CPU 调度;

  8. 重新竞争锁:被唤醒线程获得 CPU 时间片后,再次尝试加锁,成功后切回用户态,继续执行业务逻辑。

2. 核心开销量化拆解(x86 服务器场景)

操作环节 耗时量级 核心影响因素
无竞争加解锁(用户态) 5-20ns CPU 主频、原子指令开销
用户态→内核态切换 100-300ns 系统调用、特权级切换
线程上下文切换 500ns-2μs CPU 核心数、调度器负载、缓存刷新
阻塞 - 唤醒完整流程 2μs-10μs 锁竞争程度、系统负载、等待队列长度

3. 高竞争下性能恶化的核心原因

  1. 串行化瓶颈:大量线程排队等待锁,临界区完全串行化,CPU 多核并行能力完全失效,吞吐量随竞争加剧线性下降;

  2. 调度颠簸:大量线程频繁阻塞 - 唤醒,导致内核调度器负载飙升,上下文切换次数爆炸,CPU 大量时间消耗在调度而非业务执行;

  3. CPU 缓存失效:线程频繁换出换入 CPU,导致缓存行频繁冲刷、伪共享加剧,内存访问延迟大幅上升;

  4. 锁饥饿:非公平锁模式下,部分线程长期抢不到锁,导致业务延迟抖动、超时。

五、操作系统锁 工程实践核心问题与避坑指南

1. 死锁:最严重的并发 bug

(1)死锁的四大必要条件(Coffman 条件)

死锁的发生必须同时满足以下 4 个条件,缺一不可:

  1. 互斥条件:资源是排他性的,同一时间只能被一个线程持有;

  2. 持有并等待:线程持有一个资源,同时申请另一个被其他线程持有的资源,且不释放已持有的资源;

  3. 不可抢占:资源只能由持有者主动释放,不能被其他线程强制抢占;

  4. 循环等待:多个线程形成环形的资源依赖链,每个线程都在等待下一个线程持有的资源。

(2)死锁的规避方案

核心逻辑:打破四大必要条件中的任意一个,即可彻底避免死锁

打破的条件 具体工程方案
循环等待 1. 全局统一锁排序,所有线程必须按固定顺序加锁; 2. 按资源 ID 从小到大的顺序加锁,避免环形依赖
持有并等待 1. 一次性申请所有需要的资源,申请失败则释放所有已持有的资源; 2. 禁止持有锁的同时,申请其他锁
不可抢占 1. 使用带超时的trylock,加锁失败则释放所有已持有的锁,重试或退出; 2. 支持锁的抢占机制(如优先级抢占)
互斥条件 用无锁编程、TLS、单线程串行化等方案,从根源消除锁的使用

2. 优先级反转:实时系统的致命问题

(1)问题描述

高优先级线程需要等待低优先级线程持有的锁,而低优先级线程又被中等优先级线程抢占,无法执行释放锁,导致高优先级线程长期阻塞,优先级调度机制失效。

  • 经典案例:1997 年火星探路者号飞船,因优先级反转导致系统频繁重启。
(2)解决方案
  1. 优先级继承协议:当高优先级线程等待低优先级线程持有的锁时,内核临时将低优先级线程的优先级提升至高优先级线程的级别,保证其快速执行并释放锁,释放后恢复原优先级;

  2. 优先级天花板协议:为每个锁预设一个最高优先级,线程持有锁时,优先级自动提升至该天花板级别,避免被中等优先级线程抢占;

  3. 禁止在高优先级场景中使用互斥锁,改用无锁方案。

3. 惊群问题:高并发场景的性能杀手

(1)问题描述

当锁释放或条件变量广播时,内核唤醒所有等待的线程,但最终只有一个线程能抢到锁,其他线程被无效唤醒后,又会重新进入阻塞,导致大量无效的内核切换、CPU 飙升。

(2)解决方案
  1. 互斥锁场景:使用FUTEX\_WAKE仅唤醒 1 个线程,Linux 的pthread\_mutex\_unlock默认已做优化;

  2. 条件变量场景:除非必须唤醒所有线程,否则一律使用signal而非broadcast

  3. 网络 IO 场景:使用 SO_REUSEPORT 端口复用,内核仅唤醒一个进程 / 线程处理连接,避免 accept 惊群。

4. 虚假唤醒:条件变量的必踩坑

前文已详细说明,核心解决方案是必须用 while 循环包裹条件检查,而非 if 判断,唤醒后必须重新校验条件。

六、主流编程语言的锁 与 操作系统锁的映射关系

业务开发中使用的语言层锁,底层几乎都是对操作系统锁的封装,核心映射关系如下:

1. Java 语言

  • synchronized :无竞争时是偏向锁 / 轻量级锁(用户态 CAS 自旋),发生激烈竞争时,会膨胀为重量级锁 ,底层绑定 Linux 的pthread\_mutex\_t(互斥锁)+ 条件变量,完全依赖操作系统锁实现阻塞与唤醒;

  • ReentrantLock/ReentrantReadWriteLock :基于 AQS 实现,底层通过 Unsafe 类的 CAS 实现自旋,竞争失败时通过LockSupport\.park\(\)阻塞,Linux 下park\(\)底层调用FUTEX\_WAIT系统调用,本质还是操作系统锁的封装;

  • Semaphore/Condition:底层同样基于 AQS+Futex 实现,对应操作系统的信号量和条件变量。

2. C++ 语言

  • std::mutex :Linux 下直接封装pthread\_mutex\_t,底层就是操作系统互斥锁;

  • std::recursive_mutex :封装PTHREAD\_MUTEX\_RECURSIVE类型的递归互斥锁;

  • std::shared_mutex :Linux 下封装pthread\_rwlock\_t,对应操作系统读写锁;

  • std::condition_variable :封装pthread\_cond\_t,对应操作系统条件变量;

  • std::timed_mutex :封装带超时的互斥锁,底层基于FUTEX\_WAIT的超时机制实现。

3. Python/Go 等语言

  • Python 的threading\.Lock/threading\.RLock:底层封装操作系统的互斥锁 / 递归锁,CPython 的 GIL 本质也是一个操作系统互斥锁;

  • Go 的sync\.Mutex/sync\.RWMutex:底层基于 Linux 的 Futex 机制实现,无竞争时用户态自旋,竞争时陷入内核阻塞,是对操作系统锁的轻量化封装。

相关推荐
lsx2024061 小时前
Python 统计学基础与高级应用
开发语言
研究点啥好呢1 小时前
快手多模态算法工程师面试题精选:10道高频考题+答案解析
java·开发语言·人工智能·ai·面试·笔试
xxjj998a1 小时前
PHP vs C#:核心差异全解析
开发语言·c#·php
遗憾随她而去.1 小时前
Java学习(一)
java·开发语言·学习
kyriewen111 小时前
代码写成一锅粥?3个设计模式让你的项目“起死回生”
开发语言·前端·javascript·设计模式·ecmascript
陌路物是人非1 小时前
记一个controller入参为null的奇怪问题
java·开发语言
陈天伟教授1 小时前
AI 未来趋势:产业应用范式之变
大数据·开发语言·人工智能·gpt
小瓦码J码1 小时前
Spring boot 如何自定义加密解密数据库连接配置
java
XiYang-DING2 小时前
【Java EE】JUC的常见类(Callable、ReentrantLock、Semaphore和CountDownLatch )
java·java-ee