C/C++ volatile关键字原理及应用介绍

你写的应该是 volatile ,不是 volitale

volatile 是 C/C++ 里嵌入式开发非常常见的关键字。它的核心作用一句话说就是:

告诉编译器:这个变量的值可能会被"当前代码看不到的地方"改变,所以每次访问它都必须真的去内存/寄存器重新读写,不能自作聪明优化掉。


1. 为什么需要 volatile

普通变量,编译器会认为:

c 复制代码
int flag = 0;

while (flag == 0)
{
    // 等待 flag 变成 1
}

在当前这段代码里,flag 没有被修改,所以编译器可能会优化成:

c 复制代码
if (flag == 0)
{
    while (1)
    {
        // 死循环
    }
}

因为编译器觉得:

我看见的代码里面,flag 一直是 0,那我没必要每次循环都重新读它。

但是在嵌入式里,flag 可能被这些地方修改:

c 复制代码
void EXTI0_IRQHandler(void)
{
    flag = 1;
}

也就是说,主循环看不到中断函数什么时候执行,但中断确实会改变 flag

所以要写成:

c 复制代码
volatile int flag = 0;

while (flag == 0)
{
    // 等待中断把 flag 改成 1
}

这样编译器就不敢把 flag 缓存到寄存器里,也不敢把这个循环优化成死循环。它必须每次都重新读取 flag 的真实值。


2. volatile 的本质工作原理

比如这段代码:

c 复制代码
int flag = 0;

while (flag == 0)
{
}

不加 volatile,编译器可能生成类似逻辑:

asm 复制代码
LOAD flag 到寄存器 R0

loop:
    判断 R0 是否等于 0
    如果等于 0,继续跳回 loop

问题在于:

它只读了一次 flag,后面一直判断寄存器 R0 的值。

如果中断里把内存中的 flag 改成 1,R0 还是旧值 0,主循环永远不知道。

加了 volatile

c 复制代码
volatile int flag = 0;

while (flag == 0)
{
}

编译器生成逻辑会变成:

asm 复制代码
loop:
    每次都从内存重新 LOAD flag
    判断 flag 是否等于 0
    如果等于 0,继续跳回 loop

这就是 volatile 的核心作用:

禁止编译器把变量访问优化成一次读取、多次复用。


3. 嵌入式里最常见的应用场景

场景 1:中断和主程序共享变量

这是最经典用法。

c 复制代码
volatile uint8_t uart_rx_done = 0;

void USART0_IRQHandler(void)
{
    if (/* 接收完成 */)
    {
        uart_rx_done = 1;
    }
}

int main(void)
{
    while (1)
    {
        if (uart_rx_done)
        {
            uart_rx_done = 0;

            // 处理串口数据
        }
    }
}

这里 uart_rx_done 必须加 volatile

因为它在两个地方被访问:

c 复制代码
main() 主循环读取
USART0_IRQHandler() 中断里修改

如果不加,主循环可能一直读到旧值。


场景 2:硬件寄存器映射

比如 GPIO 输入寄存器、状态寄存器、串口状态寄存器、DMA 标志位寄存器等。

假设某个寄存器地址是:

c 复制代码
#define UART_STATUS_ADDR  0x40011000

你可以这样写:

c 复制代码
#define UART_STATUS   (*(volatile uint32_t *)0x40011000)

while ((UART_STATUS & (1 << 5)) == 0)
{
    // 等待串口接收完成
}

为什么这里必须是 volatile

因为 UART_STATUS 的值不是软件改的,而是硬件外设自动改的。

比如串口收到数据后,硬件会把状态寄存器的某一位置 1。

如果不加 volatile,编译器可能觉得:

c 复制代码
UART_STATUS

这个值在循环里没有被 C 代码修改,所以没必要反复读。

但实际上它会被硬件改变。

所以硬件寄存器必须用 volatile


场景 3:定时器 tick 变量

比如 SysTick 中断每 1ms 增加一次系统时间:

c 复制代码
volatile uint32_t sys_tick_ms = 0;

void SysTick_Handler(void)
{
    sys_tick_ms++;
}

void delay_ms(uint32_t ms)
{
    uint32_t start = sys_tick_ms;

    while ((sys_tick_ms - start) < ms)
    {
        // 等待时间到
    }
}

这里 sys_tick_ms 也必须加 volatile

因为它在中断里面变化,主程序里面读取。


场景 4:DMA 传输完成标志

c 复制代码
volatile uint8_t dma_tx_done = 0;

void DMA_IRQHandler(void)
{
    if (/* DMA 传输完成 */)
    {
        dma_tx_done = 1;
    }
}

void spi_dma_send(uint8_t *buf, uint16_t len)
{
    dma_tx_done = 0;

    // 启动 DMA 发送

    while (dma_tx_done == 0)
    {
        // 等待 DMA 完成
    }
}

这里 dma_tx_done 必须加 volatile

因为 DMA 完成中断会改变这个变量。


4. volatile 解决了什么问题?

它主要解决 编译器优化导致的变量读取错误问题

具体来说,它会告诉编译器:

第一,每次读变量都必须真的读

c 复制代码
volatile int flag;

下面代码:

c 复制代码
if (flag)
{
}

if (flag)
{
}

编译器不能偷懒说:

我第一次读过了,第二次直接用第一次的值吧。

它必须每次都重新读。


第二,每次写变量都必须真的写

c 复制代码
volatile uint32_t *reg = (volatile uint32_t *)0x40011000;

*reg = 0x01;
*reg = 0x02;
*reg = 0x03;

如果不是 volatile,编译器可能认为前两次写没意义,最后只保留:

c 复制代码
*reg = 0x03;

但对于硬件寄存器来说,连续写入 0x010x020x03 可能分别代表不同命令,不能省略。

加了 volatile 后,三次写都必须真实执行。


第三,不允许把变量长期缓存到 CPU 寄存器里

普通变量可能被编译器放到 CPU 寄存器里反复用。

volatile 变量每次都要重新访问它对应的内存地址或者硬件地址。


5. volatile 不能解决什么问题?

这个非常重要。

很多初学者会误以为:

加了 volatile,多线程/中断共享变量就安全了。

这是错的。

volatile 只管 编译器优化,不保证下面这些东西。


不能保证原子性

比如:

c 复制代码
volatile int count = 0;

count++;

很多人以为 volatilecount++ 就安全了,这是错的。

count++ 实际上是三步:

c 复制代码
读取 count
count 加 1
写回 count

类似:

c 复制代码
temp = count;
temp = temp + 1;
count = temp;

如果主循环和中断同时操作 count++,可能出问题。

例如:

c 复制代码
volatile int count = 0;

void IRQ_Handler(void)
{
    count++;
}

int main(void)
{
    count++;
}

假设执行顺序是:

c 复制代码
main 读取 count = 0
中断进入,读取 count = 0
中断加 1,写回 count = 1
main 恢复,加 1,写回 count = 1

理论上执行了两次 count++,结果应该是 2。

但实际结果变成 1。

所以 volatile 不保证原子操作。

要解决这个问题,需要:

c 复制代码
关中断
临界区
互斥锁
原子操作
RTOS 信号量
RTOS 队列

例如裸机里可以这样:

c 复制代码
__disable_irq();
count++;
__enable_irq();

不能代替互斥锁

在 RTOS 里面,如果两个任务共享一个变量:

c 复制代码
volatile int shared_data;

这并不代表线程安全。

如果多个任务同时读写复杂数据,比如结构体、数组、队列、链表,应该用:

c 复制代码
mutex
semaphore
queue
event group
task notification
critical section

比如 FreeRTOS 里面,任务间通信最好用:

c 复制代码
xQueueSend()
xQueueReceive()
xSemaphoreGive()
xSemaphoreTake()
xTaskNotify()
ulTaskNotifyTake()

不要单纯依赖 volatile


不能解决 CPU Cache 问题

比如 Cortex-A、Linux、ESP32-S3 某些外部 RAM、带 Cache 的 MCU,DMA 和 CPU 共享内存时:

c 复制代码
volatile uint8_t dma_buffer[1024];

这不一定够。

因为 DMA 改的是真实内存,CPU 可能读的是 Cache 里的旧数据。

这个时候需要:

c 复制代码
Cache clean
Cache invalidate
DMA buffer 放到 non-cache 区域
内存屏障

所以:

volatile 解决的是编译器优化问题,不是 Cache 一致性问题。


不能保证内存访问顺序完全符合硬件要求

有些 CPU 或编译器可能会做指令重排。
volatile 能限制编译器对 volatile 访问的优化,但在复杂系统里,访问外设寄存器前后可能还需要内存屏障。

比如 ARM 里可能需要:

c 复制代码
__DSB();
__ISB();

或者:

c 复制代码
__DMB();

特别是低功耗、DMA、Cache、MPU、多核、外设启动这类场景。


6. volatile 的典型写法

普通 volatile 变量

c 复制代码
volatile uint8_t flag;
volatile uint32_t sys_tick;
volatile int motor_speed;

含义:

这个变量的值可能随时变化,每次访问都要重新读写。


volatile 指针

c 复制代码
volatile uint32_t *reg;

意思是:

reg 指向的内容是 volatile 的。

常用于硬件寄存器:

c 复制代码
volatile uint32_t *GPIOA_IDR = (volatile uint32_t *)0x40020010;

访问:

c 复制代码
uint32_t value = *GPIOA_IDR;

每次 *GPIOA_IDR 都会真的从地址 0x40020010 读取。


指针本身是 volatile

c 复制代码
uint32_t * volatile p;

这个意思是:

指针变量 p 本身是 volatile 的,但是它指向的数据不是 volatile 的。

这种比较少见。


指针和指向的数据都是 volatile

c 复制代码
volatile uint32_t * volatile p;

意思是:

指针变量本身可能变化,指针指向的内容也可能变化。

嵌入式里偶尔会用,但不如第一种常见。


7. const volatile 是什么?

这个在寄存器里也很常见。

c 复制代码
const volatile uint32_t *status_reg = (const volatile uint32_t *)0x40011000;

含义:

c 复制代码
const    :软件不能主动写它
volatile :它可能被硬件改变

比如串口接收状态寄存器、ADC 结果寄存器、GPIO 输入寄存器。

软件只是读:

c 复制代码
uint32_t status = *status_reg;

但是硬件可以改变它。

所以它既是 const,又是 volatile


8. 硬件寄存器结构体为什么经常全是 volatile?

很多 MCU 头文件里面都有类似东西:

c 复制代码
typedef struct
{
    volatile uint32_t CTL;
    volatile uint32_t STAT;
    volatile uint32_t DATA;
    volatile uint32_t BAUD;
} USART_TypeDef;

#define USART0 ((USART_TypeDef *)0x40013800)

然后操作:

c 复制代码
USART0->DATA = data;

while ((USART0->STAT & USART_STAT_TBE) == 0)
{
}

这里每个寄存器成员都要是 volatile

因为:

c 复制代码
USART0->STAT

是硬件状态寄存器,可能被硬件自动改变。

c 复制代码
USART0->DATA

写入数据寄存器会触发硬件发送,不是普通内存赋值。


9. 常见错误写法

错误 1:中断共享变量不加 volatile

错误:

c 复制代码
uint8_t key_pressed = 0;

void EXTI_IRQHandler(void)
{
    key_pressed = 1;
}

int main(void)
{
    while (1)
    {
        if (key_pressed)
        {
            key_pressed = 0;
            // 处理按键
        }
    }
}

正确:

c 复制代码
volatile uint8_t key_pressed = 0;

错误 2:以为 volatile 可以保证线程安全

错误理解:

c 复制代码
volatile int count;

然后多个任务都 count++

这不安全。

正确方式:

c 复制代码
taskENTER_CRITICAL();
count++;
taskEXIT_CRITICAL();

或者用互斥锁、信号量、原子操作。


错误 3:DMA buffer 只加 volatile 就以为万事大吉

c 复制代码
volatile uint8_t dma_rx_buf[1024];

在无 Cache 的 MCU 上可能没问题。

但在带 Cache 的芯片上,比如高性能 Cortex-M7、Cortex-A、某些外部 PSRAM 场景,可能还要处理 Cache。

例如:

c 复制代码
SCB_InvalidateDCache_by_Addr();

或者把 DMA buffer 放到非缓存区域。


10. 一个非常经典的例子:等待中断标志

不加 volatile

c 复制代码
uint8_t rx_done = 0;

void USART_IRQHandler(void)
{
    rx_done = 1;
}

void wait_uart_rx_done(void)
{
    while (rx_done == 0)
    {
    }
}

可能出问题。

volatile

c 复制代码
volatile uint8_t rx_done = 0;

void USART_IRQHandler(void)
{
    rx_done = 1;
}

void wait_uart_rx_done(void)
{
    while (rx_done == 0)
    {
    }
}

这样才靠谱。

但是如果是复杂数据,比如接收长度:

c 复制代码
volatile uint16_t rx_len;
volatile uint8_t rx_done;
uint8_t rx_buf[128];

中断里:

c 复制代码
void USART_IRQHandler(void)
{
    rx_len = get_rx_len();
    rx_done = 1;
}

主循环:

c 复制代码
if (rx_done)
{
    rx_done = 0;
    process(rx_buf, rx_len);
}

这时候最好注意顺序和临界区。更严谨一点:

c 复制代码
uint16_t len;

__disable_irq();
if (rx_done)
{
    rx_done = 0;
    len = rx_len;
}
__enable_irq();

if (len > 0)
{
    process(rx_buf, len);
}

11. volatile 和普通变量对比

对比项 普通变量 volatile 变量
编译器是否可以优化读取 可以 不可以随便优化
是否每次都从内存/寄存器读 不一定
是否每次写都保留 不一定
适合普通计算变量 适合 不太适合
适合硬件寄存器 不适合 适合
适合中断共享标志 不适合 适合
能否保证原子性 不能 不能
能否代替锁 不能 不能
能否解决 Cache 一致性 不能 不能

12. 哪些变量应该加 volatile?

你可以记这个判断标准:

只要这个变量会被"当前执行流之外的东西"改变,就考虑加 volatile

典型包括:

c 复制代码
1. 中断函数修改,主循环读取的变量
2. 主循环修改,中断读取的变量
3. 硬件寄存器
4. DMA 完成标志
5. 定时器 tick 变量
6. 多任务共享的简单状态标志
7. 调试时观察的特殊变量

13. 哪些变量不应该乱加 volatile?

不要看到变量就加。

比如普通局部变量:

c 复制代码
void func(void)
{
    volatile int a = 0;
    a++;
}

没必要。

普通计算过程变量也不要加:

c 复制代码
volatile int temp;
volatile int sum;
volatile int average;

这样会降低优化效果,让程序变慢、代码变大。

尤其在 MCU 上,过度使用 volatile 会导致:

c 复制代码
性能下降
代码变大
功耗增加
编译器无法优化

所以原则是:

该加的必须加,不该加的别乱加。


14. 在 RTOS 里面怎么理解 volatile?

如果你用 FreeRTOS,很多时候不应该靠 volatile 做任务通信。

例如两个任务通信,不推荐:

c 复制代码
volatile uint8_t task_flag = 0;

void task1(void *arg)
{
    task_flag = 1;
}

void task2(void *arg)
{
    while (1)
    {
        if (task_flag)
        {
            task_flag = 0;
        }
    }
}

更推荐:

c 复制代码
xTaskNotifyGive(task2_handle);

或者:

c 复制代码
xQueueSend(queue, &data, portMAX_DELAY);

或者:

c 复制代码
xSemaphoreGive(semaphore);

因为 RTOS API 不只是传变量,它还处理了:

c 复制代码
任务阻塞
任务唤醒
调度切换
临界区保护
内存可见性
同步关系

volatile 只是告诉编译器不要优化变量访问。

所以 RTOS 里总结:

简单标志可以用 volatile,但正规任务同步应该用 RTOS 机制。


15. 一个嵌入式项目里的真实风格例子

比如你的串口 DMA 空闲中断接收,可以这样设计:

c 复制代码
#define RX_BUF_SIZE 128

uint8_t uart_rx_buf[RX_BUF_SIZE];

volatile uint16_t uart_rx_len = 0;
volatile uint8_t uart_rx_done = 0;

void USART_IRQHandler(void)
{
    if (/* 检测到 IDLE 空闲中断 */)
    {
        // 清除 IDLE 标志

        // 计算 DMA 已经接收到的数据长度
        uart_rx_len = RX_BUF_SIZE - dma_transfer_number_get();

        // 通知主循环:一帧数据接收完成
        uart_rx_done = 1;
    }
}

int main(void)
{
    while (1)
    {
        if (uart_rx_done)
        {
            uint16_t len;

            __disable_irq();

            uart_rx_done = 0;
            len = uart_rx_len;

            __enable_irq();

            // 处理 uart_rx_buf 里面的数据
            uart_protocol_parse(uart_rx_buf, len);

            // 重新启动 DMA 接收
            uart_dma_rx_restart();
        }
    }
}

这里:

c 复制代码
uart_rx_done
uart_rx_len

需要加 volatile

但是:

c 复制代码
uart_rx_buf

要不要加 volatile 要看情况。

如果是 DMA 写入,CPU 读取,在无 Cache 的普通 Cortex-M MCU 上,不加一般也可以。

如果你发现编译器优化导致访问异常,或者你在轮询 buffer 某些位置变化,可以考虑加。

如果是带 Cache 的芯片,光加 volatile 还不够,需要处理 Cache。


16. 最容易记住的一句话

volatile 不是让变量变"稳定",反而是告诉编译器:

这个变量很"不稳定",可能随时变,你别优化它,每次都老老实实去读写。


17. 面试/工作中的标准总结

volatile 的作用:

  1. 防止编译器优化掉对变量的读写。
  2. 保证每次访问都从变量所在地址重新读取或写入。
  3. 常用于中断共享变量、硬件寄存器、DMA/外设状态标志。
  4. 不能保证原子性。
  5. 不能保证线程安全。
  6. 不能替代锁、信号量、临界区。
  7. 不能解决 Cache 一致性问题。

18. 最后给你一个判断口诀

text 复制代码
中断会改它,加 volatile
硬件会改它,加 volatile
DMA/外设状态标志,加 volatile
普通计算变量,别乱加 volatile
多任务复杂共享,volatile 不够,要锁/队列/信号量
自增自减共享变量,volatile 不够,要临界区/原子操作
带 Cache 的 DMA,volatile 不够,要处理 Cache

嵌入式里你最常见的写法基本就是:

c 复制代码
volatile uint8_t flag;
volatile uint32_t tick;
volatile uint16_t rx_len;
#define REG (*(volatile uint32_t *)0x40000000)



对,你这个反馈很对。
volatile 不能一上来就背"防止编译器优化",这样很抽象。我们从 CPU、内存、编译器、中断、硬件寄存器 这几个东西的关系开始讲。


1. 先建立一个核心模型

你写的 C 代码不是直接给 MCU 执行的。

流程是:

text 复制代码
你写的 C 代码
        ↓
编译器分析、优化
        ↓
生成汇编/机器码
        ↓
CPU 真正执行
        ↓
读写 RAM / Flash / 外设寄存器

volatile 主要管的是中间这一步:

text 复制代码
编译器优化

它不是直接控制 CPU,也不是控制硬件,而是告诉编译器:

text 复制代码
这个变量很特殊,你不要乱优化它。

2. 编译器为什么会"乱优化"?

因为编译器很聪明,也很"自信"。

比如你写:

c 复制代码
int flag = 0;

while (flag == 0)
{
}

编译器会看这段代码:

c 复制代码
int flag = 0;

while (flag == 0)
{
}

它心里会想:

text 复制代码
flag 一开始是 0。
while 里面没有任何代码修改 flag。
那 flag 永远都是 0。
所以这个 while 就是死循环。

于是它可能优化成:

c 复制代码
while (1)
{
}

或者生成类似这样的汇编逻辑:

asm 复制代码
读取一次 flag
如果 flag 是 0,就一直原地死循环

也就是说,它可能只读取一次 flag,之后就不再读了。


3. 但是嵌入式里情况不一样

在普通电脑应用程序里,一个变量通常是当前代码自己改的。

但是在嵌入式里,一个变量可能被这些东西改变:

text 复制代码
1. 中断函数
2. DMA
3. 硬件外设
4. 另一个任务
5. 另一个 CPU 核心
6. 调试器

比如:

c 复制代码
int flag = 0;

void EXTI0_IRQHandler(void)
{
    flag = 1;
}

int main(void)
{
    while (flag == 0)
    {
    }

    // 中断来了之后,希望跳出 while
}

你作为程序员知道:

text 复制代码
flag 会在中断里被改成 1。

但是编译器看 main() 的时候,它可能没这么理解。

它只看到:

c 复制代码
while (flag == 0)
{
}

它可能认为:

text 复制代码
main 里面没人改 flag,那我没必要每次都重新读取 flag。

这就是问题根源。


4. volatile 的真正含义

写成这样:

c 复制代码
volatile int flag = 0;

意思不是:

text 复制代码
这个变量很稳定。

恰恰相反,意思是:

text 复制代码
这个变量很不稳定,可能随时被外部改变。
编译器你每次用它的时候,都必须重新去它的地址读取。
不能偷懒。
不能缓存。
不能省略。
不能合并。

所以 volatile 的通俗解释是:

text 复制代码
每次访问这个变量,都必须真的访问它的内存地址。

5. 用一个生活例子理解

假设你在等快递。

普通变量像这样:

text 复制代码
你看了一眼门口,没有快递。
然后你一直坐在屋里想:刚才没有,现在肯定也没有。
所以不再看门口。

这就是普通变量被编译器优化。

volatile 像这样:

text 复制代码
快递员可能随时来。
所以你不能只看一次。
你必须每隔一会儿真的去门口看一次。

这就是 volatile:

text 复制代码
每次都重新读取真实状态。

6. 不加 volatile 的问题,本质是"读了一次就不读了"

比如:

c 复制代码
int flag = 0;

while (flag == 0)
{
}

不加 volatile,编译器可能理解成:

c 复制代码
int temp = flag;

while (temp == 0)
{
}

也就是它把 flag 的值读到 CPU 寄存器里面:

text 复制代码
RAM 里面的 flag = 0
CPU 寄存器 temp = 0

然后后面一直判断 temp

即使中断里把 RAM 里的 flag 改成了 1:

text 复制代码
RAM 里面的 flag = 1
CPU 寄存器 temp = 0

主循环还是在判断 temp == 0

所以永远出不来。


7. 加了 volatile 之后会怎样?

c 复制代码
volatile int flag = 0;

while (flag == 0)
{
}

编译器就不敢把它变成:

c 复制代码
int temp = flag;

while (temp == 0)
{
}

它必须每次都真的读:

text 复制代码
第 1 次循环:从 flag 地址读取
第 2 次循环:从 flag 地址读取
第 3 次循环:从 flag 地址读取
第 4 次循环:从 flag 地址读取
......

所以如果中断里改了 flag

c 复制代码
void EXTI0_IRQHandler(void)
{
    flag = 1;
}

主循环下一次读取时就能发现:

text 复制代码
flag 已经变成 1 了。

于是跳出循环。


8. 所以 volatile 的第一大用途:中断共享变量

典型代码:

c 复制代码
volatile uint8_t key_pressed = 0;

void EXTI0_IRQHandler(void)
{
    key_pressed = 1;
}

int main(void)
{
    while (1)
    {
        if (key_pressed)
        {
            key_pressed = 0;

            // 处理按键事件
        }
    }
}

这里 key_pressed 必须加 volatile

因为它有两个访问者:

text 复制代码
中断函数:修改 key_pressed
主循环:读取 key_pressed

主循环不知道中断什么时候来。

编译器也不能假设这个变量不会变化。


9. 再从硬件寄存器理解 volatile

这个更嵌入式。

比如串口状态寄存器:

c 复制代码
while ((USART0->STAT & USART_STAT_RBNE) == 0)
{
    // 等待接收到数据
}

这个意思是:

text 复制代码
一直等待串口接收缓冲区非空。

USART0->STAT 是硬件寄存器。

它不是普通 RAM 变量。

它可能被硬件自动改变。

比如串口收到一个字节后,硬件会自动把状态位 RBNE 置 1。

问题来了:

如果没有 volatile,编译器可能想:

text 复制代码
while 里面没有代码修改 USART0->STAT。
那我读一次就够了。

这就坏了。

因为 USART0->STAT 是硬件改的,不是 C 代码改的。

所以硬件寄存器必须是 volatile。

常见定义大概是这样:

c 复制代码
#define USART0_STAT (*(volatile uint32_t *)0x40013800)

或者:

c 复制代码
typedef struct
{
    volatile uint32_t CTL;
    volatile uint32_t STAT;
    volatile uint32_t DATA;
} USART_TypeDef;

这里的 volatile 是在告诉编译器:

text 复制代码
这些寄存器不是普通变量。
它们可能被硬件改变。
每次访问都必须真的读写硬件地址。

10. volatile 对"读"和"写"都有影响

很多人只知道 volatile 防止"读"被优化。

其实"写"也一样。

比如:

c 复制代码
volatile uint32_t *reg = (volatile uint32_t *)0x40000000;

*reg = 1;
*reg = 2;
*reg = 3;

如果不是 volatile,编译器可能认为:

text 复制代码
前面写 1、写 2 没意义。
反正最后都是 3。
那我只保留 *reg = 3 就行。

但是对硬件寄存器来说,这三个写操作可能分别表示三个命令:

text 复制代码
写 1:复位外设
写 2:配置模式
写 3:启动外设

如果编译器省略前两次写,硬件就乱了。

加了 volatile 后,编译器必须真的执行:

text 复制代码
写 1
写 2
写 3

不能省略。


11. 用一句非常准确的话概括 volatile

volatile 的本质是:

text 复制代码
把变量访问变成"有副作用的访问"。

什么叫"有副作用"?

普通变量:

c 复制代码
a = 1;
a = 2;
a = 3;

编译器觉得只有最后一次有意义。

但是硬件寄存器:

c 复制代码
REG = 1;
REG = 2;
REG = 3;

每次写都有可能触发硬件动作。

所以不能删。

volatile 就是在告诉编译器:

text 复制代码
这个读/写动作本身就有意义。
你不能只看最终结果。

12. 现在重新理解"防止编译器优化"

这句话容易误解。

volatile 不是禁止所有优化。

它只是禁止编译器对这个变量的访问做这些优化:

text 复制代码
1. 不允许把多次读取合并成一次读取
2. 不允许把变量一直缓存到寄存器里
3. 不允许把看似没用的读取删掉
4. 不允许把看似重复的写入删掉
5. 不允许随便改变 volatile 访问之间的顺序

但是其他普通代码,该优化还是会优化。


13. volatile 不是什么?

这个非常重要。

1. volatile 不是锁

c 复制代码
volatile int count = 0;

这不代表多个任务可以安全地同时操作 count

比如:

c 复制代码
count++;

这个不是一步完成的。

它实际是:

text 复制代码
第一步:读取 count
第二步:加 1
第三步:写回 count

如果中断和主循环同时 count++,仍然可能出错。


2. volatile 不保证原子性

比如:

c 复制代码
volatile uint32_t count;

count++;

volatile 只能保证:

text 复制代码
真的读取 count
真的写回 count

但不能保证:

text 复制代码
读取、加一、写回 这三个动作中间不被中断打断

如果你要保证不被打断,要用:

c 复制代码
__disable_irq();
count++;
__enable_irq();

或者 RTOS 里面用:

c 复制代码
taskENTER_CRITICAL();
count++;
taskEXIT_CRITICAL();

3. volatile 不能代替 FreeRTOS 队列/信号量

比如任务通信:

c 复制代码
volatile uint8_t flag;

简单场景可以用。

但复杂一点最好用:

text 复制代码
队列 Queue
信号量 Semaphore
事件组 EventGroup
任务通知 TaskNotify
互斥锁 Mutex

因为这些东西不只是防优化,它们还解决:

text 复制代码
任务阻塞
任务唤醒
优先级调度
互斥保护
临界区
同步顺序

volatile 没这么强。


4. volatile 不解决 Cache 问题

这个你以后搞 ESP32、Cortex-M7、Linux SoC、DMA 的时候会遇到。

比如 DMA 把数据写到内存:

c 复制代码
volatile uint8_t dma_buf[1024];

CPU 读 dma_buf 的时候,可能读到的是 Cache 里面的旧数据。

volatile 只能约束编译器:

text 复制代码
你要真的发出读取指令。

但如果 CPU 读取时命中了 Cache,还是可能拿到旧数据。

这个时候要处理:

text 复制代码
Cache invalidate
Cache clean
non-cache 内存区域
内存屏障

所以记住:

text 复制代码
volatile 解决编译器问题。
Cache 解决 CPU/内存一致性问题。
锁/临界区解决并发问题。

这三个不是一回事。


14. 一个最适合嵌入式新手的判断方法

你看到一个变量,先问自己:

text 复制代码
这个变量会不会被"当前代码看不见的东西"改变?

如果会,就考虑 volatile

比如:

场景 要不要 volatile
中断里改,主循环里读
主循环改,中断里读
硬件寄存器
DMA 完成标志
SysTick 计数变量
普通局部计算变量 不要
普通函数内部临时变量 不要
多任务共享复杂结构体 volatile 不够,要锁
DMA buffer + Cache volatile 不够,要 Cache 维护

15. 看几个最经典的例子

例子 1:延时计数

c 复制代码
volatile uint32_t sys_tick_ms = 0;

void SysTick_Handler(void)
{
    sys_tick_ms++;
}

void delay_ms(uint32_t ms)
{
    uint32_t start = sys_tick_ms;

    while ((sys_tick_ms - start) < ms)
    {
    }
}

为什么 sys_tick_ms 要加 volatile?

因为:

text 复制代码
delay_ms() 里面没有修改 sys_tick_ms。
但是 SysTick 中断每 1ms 会修改它。

如果不加,编译器可能觉得:

text 复制代码
sys_tick_ms 在 while 里面没变。
那我就不用每次重新读。

这样延时函数可能卡死。


例子 2:串口接收完成标志

c 复制代码
volatile uint8_t uart_rx_done = 0;
volatile uint16_t uart_rx_len = 0;

void USART_IRQHandler(void)
{
    uart_rx_len = 10;
    uart_rx_done = 1;
}

int main(void)
{
    while (1)
    {
        if (uart_rx_done)
        {
            uart_rx_done = 0;

            // 处理 uart_rx_len 长度的数据
        }
    }
}

这里:

c 复制代码
uart_rx_done
uart_rx_len

都建议加 volatile

因为它们是:

text 复制代码
中断写,主循环读。

例子 3:等待硬件状态位

c 复制代码
#define UART_STAT (*(volatile uint32_t *)0x40013800)
#define UART_DATA (*(volatile uint32_t *)0x40013804)
#define UART_RBNE (1 << 5)

uint8_t uart_getchar(void)
{
    while ((UART_STAT & UART_RBNE) == 0)
    {
        // 等待硬件收到数据
    }

    return UART_DATA;
}

这里 UART_STAT 必须是 volatile。

因为:

text 复制代码
UART_STAT 的值是硬件外设改变的。
不是 C 代码改变的。

16. 再讲一个容易混的点:volatile int *p

这几个写法容易混。

写法 1:指向 volatile 数据的指针

c 复制代码
volatile uint32_t *p;

意思是:

text 复制代码
p 指向的数据是 volatile。

也就是:

c 复制代码
*p

每次都要真的读写。

这最常用于硬件寄存器。


写法 2:volatile 指针

c 复制代码
uint32_t * volatile p;

意思是:

text 复制代码
p 这个指针变量本身是 volatile。

也就是指针地址可能被外部改变。

这个比较少用。


写法 3:指针和指向的数据都是 volatile

c 复制代码
volatile uint32_t * volatile p;

意思是:

text 复制代码
p 本身可能变。
*p 指向的数据也可能变。

这个更少见。

嵌入式最常用的是第一种:

c 复制代码
volatile uint32_t *reg;

17. const volatile 怎么理解?

这个也很常见。

c 复制代码
const volatile uint32_t status_reg;

拆开看:

text 复制代码
const:软件不能随便改它
volatile:它可能被外部改变

比如某些只读状态寄存器:

text 复制代码
软件不能写它。
但是硬件可以改它。

所以它既是:

text 复制代码
const

又是:

text 复制代码
volatile

例如:

c 复制代码
#define ADC_RESULT (*(const volatile uint32_t *)0x4001244C)

含义:

text 复制代码
ADC_RESULT 软件只读。
但是 ADC 硬件转换完成后会改变它。

18. 最后用一句大白话总结

volatile 就是告诉编译器:

text 复制代码
这个变量不是普通变量。
它可能被中断、硬件、DMA、其他任务在背后修改。
所以你不要凭自己分析出来的结果乱优化。
每次读,就真的去读。
每次写,就真的去写。

但是它只解决这个问题:

text 复制代码
编译器别偷懒。

它不解决:

text 复制代码
多个地方同时改会不会冲突
一次操作会不会被中断打断
Cache 里面是不是旧数据
RTOS 任务之间怎么同步

19. 你可以这样记

text 复制代码
volatile 管"看得见看不见"的问题:
    编译器看不见中断、硬件、DMA 的变化,所以要 volatile。

临界区/锁 管"会不会被打断"的问题:
    count++ 这种操作可能被中断打断,所以要临界区。

Cache 维护 管"读到的是不是最新内存"的问题:
    DMA 和 CPU 共享内存时,光 volatile 不够。

最核心的一句话:

text 复制代码
volatile 不是保证变量安全,
而是保证编译器每次都老老实实访问这个变量。



你说得对。

真正理解 volatile,不要从"概念"开始,而要从这个问题开始:

为什么我明明写了 while(flag == 0),中断里也把 flag = 1 了,程序却可能永远卡在 while 里?

这个问题搞懂了,volatile 就真的懂了。


1. 先忘掉 C 变量,MCU 实际只认识三样东西

MCU 不认识什么 int flagwhileif

MCU 只认识类似这种机器动作:

asm 复制代码
从某个地址读一个值到寄存器
比较寄存器里的值
根据比较结果跳转
把寄存器里的值写回某个地址

比如:

c 复制代码
int flag = 0;

while (flag == 0)
{
}

你脑子里想的是:

text 复制代码
一直去看 flag 的值
如果 flag 变成 1,就跳出来

但 MCU 真正执行的是编译器生成的机器指令。

所以关键不是你 C 代码"看起来"是什么意思,而是:

编译器最终有没有生成"每次循环都重新读取 flag"的指令。


2. 不加 volatile 时,编译器可能这样理解

假设 flag 在 RAM 里,地址是:

text 复制代码
flag 地址 = 0x20000000

你写:

c 复制代码
int flag = 0;

while (flag == 0)
{
}

编译器分析这段代码时,会想:

text 复制代码
flag 初始值是 0。
while 循环里面没有任何代码修改 flag。
那 flag 永远是 0。
所以没必要每次都重新读 flag。

于是它可能生成类似这种逻辑:

asm 复制代码
LDR R0, [0x20000000]   ; 只读取一次 flag 到 R0
CMP R0, #0             ; 比较 R0 是否等于 0
BNE exit               ; 如果不是 0,就跳出

loop:
B loop                 ; 如果一开始是 0,就永远死循环

exit:

注意重点:

text 复制代码
它只读了一次 flag。
后面一直在死循环。

这时候就算中断里把 RAM 里的 flag 改成 1:

text 复制代码
RAM[0x20000000] = 1

CPU 也不知道。

因为 CPU 当前死循环根本没有再执行:

asm 复制代码
LDR R0, [0x20000000]

也就是说,它没有再去 RAM 里看 flag


3. 加 volatile 之后,编译器被迫这样生成

你写成:

c 复制代码
volatile int flag = 0;

while (flag == 0)
{
}

意思就是告诉编译器:

text 复制代码
flag 这个东西不能按普通变量看待。
它可能在你看不见的地方被改变。
每次用它,都必须真的去它的地址读。

于是编译器应该生成类似这种逻辑:

asm 复制代码
loop:
LDR R0, [0x20000000]   ; 每次循环都重新读取 flag
CMP R0, #0
BEQ loop               ; 如果还是 0,继续循环

exit:

这就不一样了。

现在如果中断里执行:

c 复制代码
flag = 1;

RAM 里变成:

text 复制代码
RAM[0x20000000] = 1

主循环下一次执行:

asm 复制代码
LDR R0, [0x20000000]

就能读到 1,然后跳出循环。

所以 volatile 的工作原理,不是让变量有什么"魔法",而是:

强迫编译器每次都生成真实的 load/store 访问指令。


4. 关键点:C 代码里的"变量"运行时不一定存在

这个很重要。

你写:

c 复制代码
int a = 10;
int b = a + 1;

你以为运行时一定有个内存变量 a

但编译器可能直接优化成:

c 复制代码
int b = 11;

甚至 a 根本不会出现在最终机器码里。

普通变量只是给程序员看的"逻辑名字"。

编译器可以把它:

text 复制代码
放 RAM
放 CPU 寄存器
直接替换成常量
完全优化掉

只要最终结果在 C 语言规则里一样,编译器就可以这么干。

但是 volatile 变量不一样。

对于:

c 复制代码
volatile int flag;

每一次:

c 复制代码
flag

都代表一次真实访问。

每一次:

c 复制代码
flag = 1;

都代表一次真实写入。

编译器不能随便省略。


5. volatile 不是 CPU 功能,是编译器功能

这个一定要分清楚。

volatile 不会让 CPU 多一个硬件模式。

它不是:

text 复制代码
CPU 的特殊指令
RAM 的特殊属性
中断控制器的特殊功能

它主要影响的是:

text 复制代码
编译器生成什么机器码

普通变量:

c 复制代码
int flag;

编译器可以想办法少读、少写、缓存到寄存器。

volatile 变量:

c 复制代码
volatile int flag;

编译器必须老老实实生成访问指令。

所以更准确地说:

text 复制代码
volatile 是程序员和编译器之间的约定。

你告诉编译器:

text 复制代码
这个对象背后可能有你不知道的变化。
你别自作聪明。

6. 为什么编译器敢优化?因为它按"封闭世界"思考

编译器分析普通 C 代码时,大致认为:

text 复制代码
变量的变化来自当前程序里的语句。

比如:

c 复制代码
int flag = 0;

while (flag == 0)
{
}

编译器看到:

text 复制代码
循环里面没有 flag = xxx;
也没有函数调用可能修改 flag;
所以 flag 不变。

但是嵌入式里不是封闭世界。

还有这些"外部力量":

text 复制代码
中断可以突然修改变量
硬件外设可以自动改变寄存器
DMA 可以自己搬数据
另一个 CPU 核心可以改内存
调试器也可能改内存

这些东西不一定体现在普通 C 代码流程里。

所以你必须用 volatile 告诉编译器:

text 复制代码
别只看当前 C 代码,这个变量可能被外部改变。

7. 用中断彻底推一遍

代码:

c 复制代码
uint8_t rx_done = 0;

void USART_IRQHandler(void)
{
    rx_done = 1;
}

int main(void)
{
    while (rx_done == 0)
    {
    }

    // 接收完成
}

你脑子里的真实系统是:

text 复制代码
main 正在 while 等待
串口收到数据
硬件触发中断
CPU 跳去执行 USART_IRQHandler
rx_done = 1
CPU 返回 main
main 发现 rx_done 变成 1
跳出 while

但编译器看 main() 时,可能只看到:

c 复制代码
while (rx_done == 0)
{
}

它不会自然地把硬件中断执行流程塞进来分析。

所以它可能认为:

text 复制代码
rx_done 在 main 里没被修改。
那我读一次就够了。

于是程序卡死。

改成:

c 复制代码
volatile uint8_t rx_done = 0;

后,编译器就不能这么想了。

它必须每次循环都重新读 rx_done

这才符合真实硬件世界。


8. 再从硬件寄存器理解,这个更本质

比如串口状态寄存器。

假设有个地址:

text 复制代码
0x40013800

这个地址不是普通 RAM,而是 USART 状态寄存器。

你写:

c 复制代码
#define USART_STAT (*(uint32_t *)0x40013800)

while ((USART_STAT & (1 << 5)) == 0)
{
}

你想表达的是:

text 复制代码
不停读取串口状态寄存器
直到第 5 位变成 1

但如果没有 volatile,编译器会把它当成普通内存地址。

它可能想:

text 复制代码
这个地址里的值在 while 里没有被 C 代码修改。
我读一次就行。

于是可能变成:

asm 复制代码
LDR R0, [0x40013800]   ; 只读一次状态寄存器
TST R0, #(1 << 5)
BNE exit

loop:
B loop

这就废了。

因为串口状态位是硬件改变的。

正确写法必须是:

c 复制代码
#define USART_STAT (*(volatile uint32_t *)0x40013800)

while ((USART_STAT & (1 << 5)) == 0)
{
}

这样每次循环都会重新访问硬件寄存器:

asm 复制代码
loop:
LDR R0, [0x40013800]   ; 每次都重新读硬件状态
TST R0, #(1 << 5)
BEQ loop

9. 硬件寄存器和普通 RAM 最大区别是什么?

普通 RAM:

c 复制代码
a = 1;
a = 2;
a = 3;

编译器可以优化成:

c 复制代码
a = 3;

因为前两次写入没有意义,最后结果都是 3。

但硬件寄存器不是这样。

比如:

c 复制代码
REG = 1;
REG = 2;
REG = 3;

这三次写可能分别表示:

text 复制代码
写 1:复位模块
写 2:配置模式
写 3:启动模块

如果编译器只保留:

c 复制代码
REG = 3;

硬件流程就完全错了。

所以硬件寄存器必须是:

c 复制代码
volatile

因为对硬件来说:

每一次读写动作本身就有意义,不只是最后的数值有意义。

这句话非常关键。


10. volatile 的真正本质:让访问动作本身变得不可省略

普通变量,编译器关心的是:

text 复制代码
最终计算结果对不对

volatile 变量,编译器还必须关心:

text 复制代码
每一次访问动作有没有真实发生

比如:

c 复制代码
volatile uint32_t *reg = (volatile uint32_t *)0x40000000;

*reg = 1;
*reg = 2;
*reg = 3;

编译器必须真的生成三次写:

asm 复制代码
STR #1, [0x40000000]
STR #2, [0x40000000]
STR #3, [0x40000000]

不能合并。

再比如:

c 复制代码
volatile uint32_t value;

uint32_t a = value;
uint32_t b = value;

编译器必须生成两次读。

因为第一次读和第二次读的结果可能不同。


11. 那普通变量为什么可以只读一次?

比如:

c 复制代码
int x;

int a = x;
int b = x;

如果中间没有任何代码修改 x,编译器可能认为:

text 复制代码
a 和 b 一定一样。

于是生成:

asm 复制代码
LDR R0, [x]
MOV a, R0
MOV b, R0

只读一次。

但如果是:

c 复制代码
volatile int x;

int a = x;
int b = x;

编译器必须认为:

text 复制代码
第一次读 x,可能是 10。
第二次读 x,可能已经变成 20。

所以必须读两次:

asm 复制代码
LDR R0, [x]
MOV a, R0

LDR R1, [x]
MOV b, R1

12. volatile 到底限制了哪些优化?

从工作原理上看,它限制的是这些优化:

1. 不能把多次读合并成一次

c 复制代码
volatile int x;

a = x;
b = x;

必须读两次。


2. 不能把循环里的读提前到循环外

普通变量可能这样优化:

c 复制代码
while (x == 0)
{
}

变成:

c 复制代码
temp = x;

while (temp == 0)
{
}

volatile 不允许。


3. 不能删除看似没用的读

普通变量:

c 复制代码
int x;
x;

这个读没有用,编译器可以删。

volatile:

c 复制代码
volatile int x;
x;

这个读不能删。

因为读取硬件寄存器本身可能有意义。

有些寄存器是:

text 复制代码
读一次就清除某个标志位

所以读动作不能删。


4. 不能删除看似重复的写

普通变量:

c 复制代码
x = 1;
x = 2;

可能只保留:

c 复制代码
x = 2;

volatile 不允许。


5. 不能把 volatile 访问顺序随便乱改

比如:

c 复制代码
REG1 = 1;
REG2 = 2;

如果都是 volatile,编译器不能随便调换这两个访问顺序。

因为硬件初始化顺序可能很重要。


13. 但是 volatile 没有你想象得那么强

理解原理后,你就知道它的边界。

它只是在说:

text 复制代码
编译器,你要真的读写。

但它没有说:

text 复制代码
这个操作不会被中断打断。
这个变量多任务访问一定安全。
CPU cache 一定是最新的。
DMA 和 CPU 一定同步。

14. 为什么 volatile 不能保证 count++ 安全?

看这个:

c 复制代码
volatile int count = 0;

count++;

你以为这是一步。

其实机器层面大概率是三步:

asm 复制代码
LDR R0, [count]   ; 读 count
ADD R0, R0, #1    ; 加 1
STR R0, [count]   ; 写回 count

如果主循环正在执行:

asm 复制代码
LDR R0, [count]   ; 读到 0

突然中断来了:

asm 复制代码
LDR R1, [count]   ; 中断也读到 0
ADD R1, R1, #1
STR R1, [count]   ; 写回 1

中断返回后,主循环继续:

asm 复制代码
ADD R0, R0, #1
STR R0, [count]   ; 也写回 1

结果:

text 复制代码
主循环加了一次
中断加了一次
理论应该是 2
实际却是 1

这不是编译器偷懒的问题。

这里编译器已经真的读写了。

问题是:

text 复制代码
读-改-写这个过程被中断打断了。

所以 volatile 解决不了。

要解决这个问题,需要:

c 复制代码
__disable_irq();
count++;
__enable_irq();

或者 RTOS 临界区、互斥锁、原子操作。


15. 所以 volatile、锁、Cache 是三个不同层面

这个你一定要分清。

volatile 解决的是:

text 复制代码
编译器会不会省略/合并/缓存访问

临界区/锁解决的是:

text 复制代码
多个执行流同时改,会不会打架

Cache 维护解决的是:

text 复制代码
CPU 看到的是不是内存里的最新数据

比如 DMA 场景:

c 复制代码
volatile uint8_t dma_buf[1024];

volatile 只能保证 CPU 真的去读 dma_buf

但如果 CPU 读到的是 Cache 里的旧数据,那还是错。

这个时候要做 Cache invalidate,或者把 buffer 放到 non-cache 区域。

所以:

text 复制代码
volatile 不是万能同步工具。
它只是防止编译器装傻。

16. 更准确的一句话

不要记:

text 复制代码
volatile 防止编译器优化

这句话太粗糙。

你应该理解成:

text 复制代码
volatile 告诉编译器:
这个对象的每一次读写,都是程序必须真实发生的外部可观察动作。
你不能因为自己分析不到变化,就省略、合并、缓存或者移动这些访问。

说白了:

text 复制代码
普通变量:编译器可以只关心最后结果。
volatile:编译器必须保留每一次访问过程。

17. 你以后看到代码应该这样分析

比如:

c 复制代码
volatile uint8_t uart_rx_done;

不要机械记"中断变量加 volatile"。

你要这样想:

text 复制代码
uart_rx_done 的写入发生在中断里。
main 的 while/if 里读取它。
main 代码表面上看不到中断什么时候改它。
如果不加 volatile,编译器可能把读取优化掉。
所以要加 volatile,让 main 每次判断都重新读取真实内存。

比如:

c 复制代码
#define GPIO_IDR (*(volatile uint32_t *)0x40020010)

你要这样想:

text 复制代码
0x40020010 不是普通 RAM。
它连接的是 GPIO 输入寄存器。
引脚电平变化时,硬件会改变这个寄存器值。
C 代码里看不到是谁修改它。
所以必须 volatile。

比如:

c 复制代码
volatile uint32_t sys_tick;

你要这样想:

text 复制代码
sys_tick 在 SysTick 中断里每 1ms 增加。
delay_ms() 里面只读不写。
如果不加 volatile,编译器可能认为 while 里面 sys_tick 不变。
所以 delay_ms 可能卡死。

18. 最后用一个核心图理解

text 复制代码
普通变量:

C 代码
  ↓
编译器分析:这个值我能推出来
  ↓
可能少读、少写、放寄存器、删掉访问
  ↓
CPU 执行优化后的代码


volatile 变量:

C 代码
  ↓
编译器分析:这个值可能被外部改变,访问本身有意义
  ↓
必须保留每一次读写
  ↓
CPU 每次真的执行 load/store

19. 真正的本质总结

volatile 不是为了考试记概念。

它是为了解决 C 语言世界和嵌入式硬件世界之间的矛盾。

C 语言普通变量的世界里:

text 复制代码
变量只会被当前程序逻辑修改。
编译器可以根据代码推导变量值。

嵌入式硬件世界里:

text 复制代码
中断可能突然改变量。
硬件可能自动改寄存器。
DMA 可能背后改内存。
读写寄存器本身可能触发硬件动作。

volatile 就是在这两个世界之间打一个补丁:

text 复制代码
编译器,你不要只按照普通 C 代码推理。
这个对象背后连着真实硬件/中断/外部事件。
每次访问都必须真的发生。

所以你真正该记住的不是"防止优化",而是:

普通变量的访问是为了得到一个值;volatile 变量的访问本身就是一件必须发生的事情。

相关推荐
Henray20242 小时前
三个线程交替打印ABC
java·面试
凯瑟琳.奥古斯特2 小时前
SpringBoot快速入门指南
java·开发语言·spring boot·后端·spring
我头发还没掉光~2 小时前
P4147 玉蟾宫
数据结构·c++·算法
是席木木啊2 小时前
Tomcat CVE-2026-34483安全漏洞警告问题总结与修复方案
java·tomcat·firefox
代码漫谈2 小时前
基于 Spring Boot 3.2.x 的 Actuator 监控指南:从健康检查到企业级监控体系
java·spring boot·actuator 监控
枕星而眠2 小时前
栈(Stack)与队列(Queue)核心知识总结
c语言·数据结构·后端·链表
江屿风2 小时前
【c++笔记】类和对象流食般投喂(上)
开发语言·c++·笔记
WL_Aurora2 小时前
Java基础知识超详细总结(从入门到精通)
java
咖啡八杯2 小时前
GoF设计模式——抽象工厂模式
java·后端·spring·设计模式·抽象工厂模式