线程定义
- Linux中进程的最小执行单位就是线程,一个进程可以包含一个或多个线程,但至少会有一个线程,如下图所示:
线程的常见模型
三种模型
- 多进程模式:每个进程只有一个线程;
- 多线程模式:一个进程有多个线程;
- 多进程+多线程模式:复杂度最高;
线程安全中的线程模型
- 多线程实际的模型如下:
- 该模型下,多个线程间共享代码、数据和文件,本文探讨的线程安全,也是基于该模型。
线程安全的定义
维基百科
- 线程安全是适用于多线程代码的计算机编程概念,其仅以确保所有线程正常运行并满足其设计规范而没有意外交互的方式操作共享数据结构。
- 一个程序可以在共享地址空间中同时在多个线程中执行代码,其中每个线程都可以访问几乎所有其他线程的内存。 线程安全是一种属性,它允许代码通过同步的方式重新建立实际控制流和程序文本之间的一些对应关系,从而在多线程环境中运行。
《Java并发编程实践》
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
《The Linux Programming Interface》
- 如果一个函数可以被多个线程安全地调用,则称该函数是线程安全的; 反之,如果一个函数不是线程安全的,那么当它在另一个线程中执行时,我们就不能从一个线程调用它。
线程安全的常见场景
- 多处理器(MP)下的多线程最为典型。在同一个进程环境下,运行在不同处理器上的不同线程可能会同时访问临界资源,从而引出诸如临界区、互斥/竞争访问的概念。
- 即便在单处理器(UP)下上述问题也是存在的,如果没有对临界资源的synchronization,可能处理器在运行一个线程的临界区代码时(由于外部中断触发的调度等等)被切换到相同进程的另一个线程,也进入了相同资源的临界区,从而危害到线程安全。
线程安全的不同层级
- 库函数级别:可以提供一定的线程安全保证;
- 架构层面:防止或限制不同形式死锁风险的设计,以及最大化并发性能的优化等;
达成线程安全的方法
避免共享状态的情况
- 可重入:
将状态信息保存在每次执行的本地变量中,通常在堆栈上,而不是静态或全局变量或其他非本地状态。所有非本地状态都必须通过原子操作访问,并且数据结构也必须是可重入的。
- 线程本地存储
变量是本地化的,因此每个线程都有自己的私有副本。
- 不可变对象
对象的状态在构造后无法更改。这意味着只共享只读数据并获得固有的线程安全性。然后可以以创建新对象而不是修改现有对象的方式实现可变(非常量)操作。
同步相关,用于无法避免共享状态的情况:
- 互斥
- 对共享数据的访问使用确保在任何时候只有一个线程读取或写入共享数据的机制进行序列化。
- 互斥的结合需要经过深思熟虑,因为不正确的使用会导致死锁、活锁和资源匮乏等副作用。
- 原子操作
- 通过使用不能被其他线程中断的原子操作来访问共享数据。 这通常需要使用特殊的机器语言指令,这些指令可能在运行时库中可用。
- 由于操作是原子的,因此共享数据始终保持有效状态,无论其他线程如何访问它。 原子操作构成了许多线程锁定机制的基础,并用于实现互斥原语。
线程安全的例子
- 例1、increment_counter函数是线程安全的
# include <pthread.h> int increment_counter () { static int counter = 0; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // only allow one thread to increment at a time pthread_mutex_lock(&mutex); ++counter; // store value before any other threads increment it further int result = counter; pthread_mutex_unlock(&mutex); return result; }
例2、Counter类的inc方法是线程安全的
class Counter { private int i = 0; public synchronized void inc() { i++; } }