很多程序员在学习并发编程时都会遇到两个高度相似、却总也分不清楚的概念线程安全和可重入。 有人说 “可重入就是线程安全”有人说 “线程安全一定可重入”其实这两个概念既有关联又有本质区别 —— 一个解决 “多人抢公共资源” 的问题一个解决 “自己打断自己” 的问题。 本文将通过生活类比、代码实例和结构化图示把这两个高频面试考点彻底讲明白。一、生活类比一分钟建立直觉认知先抛开代码用两个生活化场景帮你建立最直观的理解。1. 可重入自带餐具的单人餐想象你正用自己的筷子吃饭吃到一半电话突然响了你放下筷子去接电话接完回来拿起筷子接着吃饭还是那碗饭筷子还是你的筷子完全不影响继续进食。这就是可重入的本质函数执行到一半被中断硬件中断、信号触发、递归调用都算中断结束后回来继续执行结果依然完全正确。 核心特征所有数据都 “自带”栈上局部变量、调用方传入参数不碰公共资源自己打断自己不会出问题。2. 线程安全带门锁的公共厕所小区里只有一个公共厕所一次只能进一个人。如果没有门两个人同时往里冲必然乱套。于是大家装了门锁一个人进去就锁门外面的人排队等候里面的人出来解锁下一个人再进入。这就是线程安全的本质多个线程同时调用同一个函数因为有 “锁” 或其他同步机制保护共享资源最终结果和串行执行完全一致。 核心特征允许多线程使用共享资源但必须按规则排队不能同时写入。到这里你应该已经能感受到两者的核心差异可重入关心自己能不能打断自己打断了还能不能正常跑线程安全关心多个人能不能一起用一起用会不会乱套二、正式定义与核心要求什么是可重入函数可重入Reentrant指一个函数在执行过程中可以被中断如硬件中断、操作系统信号、递归调用在中断处理程序中又被再次调用重入当中断返回后原函数的执行依然能得到正确结果。成为可重入函数的核心条件不使用全局变量、静态变量原子操作保护除外工程中极少使用不修改自身代码段仅调用其他可重入函数只使用栈上的局部变量或由调用方传入的外部数据一句话总结不持有任何共享状态所有数据都来自调用方或自身栈空间。什么是线程安全线程安全Thread-safe指在多线程环境下一个函数被多个线程并发调用时始终能返回正确结果不会出现数据错乱、竞态条件等异常。实现线程安全的常见手段对共享资源加互斥锁、信号量使用线程局部存储TLS对共享变量使用原子操作只读不写的共享数据天然线程安全一句话总结允许多线程访问共享资源但通过同步机制避免并发写入。三、四大组合场景 代码实例这是最容易踩坑的核心知识点可重入和线程安全不是包含关系而是交叉关系。一共存在四种组合我们逐个用代码拆解。场景 1既可重入又线程安全 —— 纯函数最理想的情况函数只使用参数和栈上局部变量不触碰任何全局 / 静态数据。c运行// 纯加法函数既可重入又线程安全 int add(int a, int b) { int result a b; // 局部变量存储在调用栈上 return result; }可重入性执行到一半被打断重入后栈上有独立的result副本互不影响线程安全每个线程调用都有独立栈帧无共享资源不存在竞态这类函数也叫 “纯函数”是并发编程中最安全的函数形态。场景 2可重入但线程不安全很多人想不到可重入的函数居然可能线程不安全 关键在于函数本身没有共享数据但它操作的是调用方传入的共享内存。c运行// 交换两个整数可重入但线程不安全 void swap(int *a, int *b) { int tmp *a; // 第一步读取a的值 *a *b; // 第二步写入a *b tmp; // 第三步写入b }为什么可重入函数内只有局部变量tmp没有全局 / 静态数据。就算执行到一半被信号中断信号处理中再调用swap也会使用新的栈帧tmp互不干扰原函数返回后执行依然正确。为什么线程不安全如果两个线程同时传入同一个 a、b 的内存地址就会出现竞态线程 1 刚读完*a还没写完线程 2 也来读取*a最终交换结果就会出错。一句话记住可重入只保证 “自己打断自己没问题”不保证 “别人同时来抢也没问题”。场景 3线程安全但不可重入面试最高频考点这是最经典的 “反直觉” 坑点加了锁实现线程安全的函数反而变得不可重入了c运行#include pthread.h static pthread_mutex_t lock PTHREAD_MUTEX_INITIALIZER; static int counter 0; // 计数器线程安全但不可重入 int get_count(void) { pthread_mutex_lock(lock); // 加锁 counter; int res counter; pthread_mutex_unlock(lock); // 解锁 return res; }为什么线程安全互斥锁保护了全局变量counter多线程同时调用也不会出现计数错乱。为什么不可重入如果函数执行到lock之后、unlock之前突然触发了系统信号而信号处理函数里又调用了get_count就会发生主流程已经拿到了锁还没释放信号处理中再次调用get_count又去申请同一把普通互斥锁普通互斥锁不支持递归加锁程序直接死锁这就是经典的 “自己锁死自己”——实现线程安全的锁恰恰成了可重入的障碍。场景 4既不可重入也线程不安全最糟糕的情况使用静态 / 全局变量还不加任何同步保护。c运行// 既不可重入也线程不安全 int bad_counter(void) { static int cnt 0; cnt; return cnt; }不可重入执行到一半被打断重入静态变量cnt会被累加两次原函数的结果就会异常线程不安全多线程同时调用时cnt会出现竞态导致计数丢失四、核心区别对比表格对比维度可重入函数线程安全函数核心关注点单线程 / 单执行流内的中断重入多线程并发访问共享资源态度尽量不使用任何共享状态可以使用但必须同步保护锁的使用不能使用普通互斥锁会死锁常用互斥锁、原子操作实现信号处理场景可以安全使用绝大多数不能安全使用解决的问题自己打断自己不出错多人同时抢不出错五、知识思维导图下面是完整的知识结构思维导图帮你快速梳理全部核心要点。六、常见误区澄清误区可重入函数一定线程安全错误。参考场景 2当函数操作调用方传入的共享内存指针时自身可重入但多线程并发调用会出现竞态。误区线程安全函数一定可重入错误。参考场景 3使用普通互斥锁实现的线程安全函数在中断重入时会发生死锁不可重入。误区能递归调用就是可重入函数错误。递归只是自己调用自己的一种形式如果函数内部有静态 / 全局变量递归调用也会导致数据错乱并不是可重入函数。误区信号处理函数里可以调用线程安全函数不可以。信号处理函数要求必须调用可重入函数绝大多数线程安全函数因为使用了锁在信号上下文调用会引发死锁。总结最后用一句话帮你彻底记住两者的关系可重入是更严格的 “单线程安全”线程安全是多线程场景下的并发安全可重入的函数未必线程安全线程安全的函数也未必可重入。谢谢