可重入性和线程安全的关系

一、什么是线程安全?

      线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

      比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. Items[Size] 的位置存放此元素;2.增大 Size的值。在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置0,而且Size=1;而如果是在多线程情况下,比如有两个线程,线程A 先将元素1存放在位置0。但是此时CPU 调度线程A暂停,线程B 得到运行的机会。线程B向此ArrayList 添加元素2,因为此时Size 仍然等于0 (注意,我们假设的是添加一个元素是要两个步骤,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加Size 的值,结果Size等于2。那好,我们来看看ArrayList 的情况,期望的元素应该有2个,而实际只有一个元素,造成丢失元素,而且Size等于 2。这就是“线程不安全”了

    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

安全性:   

类要成为线程安全的,首先必须在单线程环境中有正确的行为。如果一个类实现正确(这是说它符合规格说明的另一种方式),那么没有一种对这个类的对象的操作序列(读或者写公共字段以及调用公共方法)可以让对象处于无效状态,观察到对象处于无效状态、或者违反类的任何不可变量、前置条件或者后置条件的情况。此外,一个类要成为线程安全的,在被多个线程访问时,不管运行时环境执行这些线程有什么样的时序安排或者交错,它必须仍然有如上所述的正确行为,并且在调用的代码中没有任何额外的同步。其效果就是,在所有线程看来,对于线程安全对象的操作是以固定的、全局一致的顺序发生的。正确性与线程安全性之间的关系非常类似于在描述ACID(原子性、一致性、独立性和持久性)事务时使用的一致性与独立性之间的关系:从特定线程的角度看,由不同线程所执行的对象操作是先后(虽然顺序不定)而不是并行执行的。

安全程度:
    线程安全性不是一个非真即假的命题。 Vector 的方法都是同步的,并且 Vector 明确地设计为在多线程环境中工作。但是它的线程安全性是有限制的,即在某些方法之间有状态依赖(类似地,如果在迭代过程中 Vector 被其他线程修改,那么由 Vector.iterator() 返回的 iterator会抛出ConcurrentModificationException)。对于 Java 类中常见的线程安全性级别,没有一种分类系统可被广泛接受,不过重要的是在编写类时尽量记录下它们的线程安全行为。Bloch 给出了描述五类线程安全性的分类方法:不可变、线程安全、有条件线程安全、线程兼容和线程对立。只要明确地记录下线程安全特性,那么您是否使用这种系统都没关系。这种系统有其局限性 -- 各类之间的界线不是百分之百地明确,而且有些情况它没照顾到 -- 但是这套系统是一个很好的起点。这种分类系统的核心是调用者是否可以或者必须用外部同步包围操作(或者一系列操作)。下面几节分别描述了线程安全性的这五种类别。
①不可变
    不可变的对象一定是线程安全的,并且永远也不需要额外的同步[1]  。因为一个不可变的对象只要构建正确,其外部可见状态永远也不会改变,永远也不会看到它处于不一致的状态。Java 类库中大多数基本数值类如 Integer 、 String 和 BigInteger 都是不可变的。需要注意的是,对于Integer,该类不提供add方法,加法是使用+来直接操作。而+操作是不具线程安全的。这是提供原子操作类AtomicInteger的原因。
②线程安全
    线程安全的对象具有在上面“线程安全”一节中描述的属性 -- 由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要任何额外的同步。这种线程安全性保证是很严格的 -- 许多类,如 Hashtable 或者 Vector 都不能满足这种严格的定义。
③有条件的
    有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外部同步。条件线程安全的最常见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器 -- 由这些类返回的 fail-fast 迭代器假定在迭代器进行遍历的时候底层集合不会有变化。为了保证其他线程不会在遍历的时候改变集合,进行迭代的线程应该确保它是独占性地访问集合以实现遍历的完整性。通常,独占性的访问是由对锁的同步保证的 -- 并且类的文档应该说明是哪个锁(通常是对象的内部监视器(intrinsic monitor))。如果对一个有条件线程安全类进行记录,那么您应该不仅要记录它是有条件线程安全的,而且还要记录必须防止哪些操作序列的并发访问。用户可以合理地假设其他操作序列不需要任何额外的同步。
④线程兼容
    线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。这可能意味着用一个 synchronized 块包围每一个方法调用,或者创建一个包装器对象,其中每一个方法都是同步的(就像 Collections.synchronizedList() 一样)。也可能意味着用 synchronized 块包围某些操作序列。为了最大程度地利用线程兼容类,如果所有调用都使用同一个块,那么就不应该要求调用者对该块同步。这样做会使线程兼容的对象作为变量实例包含在其他线程安全的对象中,从而可以利用其所有者对象的同步。
许多常见的类是线程兼容的,如集合类 ArrayList 和 HashMap 、 java.text.SimpleDateFormat 、或 JDBC 类 Connection 和 ResultSet 。
⑤线程对立
    线程对立类是那些不管是否调用了外部同步都不能在并发使用时安全地呈现的类。线程对立很少见,当类修改静态数据,而静态数据会影响在其他线程中执行的其他类的行为,这时通常会出现线程对立。线程对立类的一个例子是调用 System.setOut() 的类。

二、可重入函数与线程安全的关系

   可重入性和线程安全性均与函数处理资源的方式有关。 但是,它们是不同的:

可重入函数既不会在连续调用中存储静态数据,也不会返回指向静态数据的指针。 对于这种类型的函数,调用方将提供函数所需的所有数据,如指向任何工作区的指针。 这意味着,函数的多个并发调用不会相互干扰。

 

注意:可重入函数不能调用非可重入函数。 

 

线程安全函数使用锁 保护共享资源,以防止对其进行并发访问。线程安全性只涉及函数实现方式,而不涉及其外部接口。 C 中,局部变量是在堆栈上动态分配的。 因此,任何不使用静态数据或其他共享资源的函数通常都是线程安全的。

 

ARM 库中,函数可能是线程安全的,如下所示:

某些函数从来都不是线程安全的,例如 setlocale()

某些函数在本质上就是线程安全的,例如 memcpy()

某些函数(例如 malloc())可通过实现 _mutex_* 函数变为线程安全的函数

其他函数仅在传递了适当参数时才是线程安全的,例如 tmpnam()。

如果应用程序以隐藏方式使用 ARM 库(如使用语言辅助函数),则可能会出现线程问题。

 

线程安全的函数

 

 

calloc(),

free(),

malloc(),

realloc() 

如果实现了 _mutex_* 函数,则堆函数是线程安全的。

 

在所有线程之间共享单个堆,并使用互斥量以避免进行并发访问时发生数据损坏。 每个堆实现都负责进行自己的锁定。 如果您提供了自己的分配器,它也必须进行自己的锁定。这样,它便可进行精细锁定(如果需要),而不是简单地使用单个互斥量保护整个堆(粗放锁定)。

__alloca(),

__alloca_finish(),

__alloca_init(),

__alloca_initialize() 

如果实现了 _mutex_* 函数,则 alloca 函数是线程安全的。

 

每个线程的 alloca 状态包含在 __user_perthread_libspace 块中。 这意味着多个线程不会发生冲突。

请注意,alloca 函数也使用堆。 不过堆函数都是线程安全的。

 

abort(),

raise(),

signal(),

fenv.h 

ARM 信号处理函数和 FP 异常捕获是线程安全的。

 

信号处理程序和 FP 捕获设置是整个进程中的全局设置,并使用锁对其进行保护。 这样,即使多个线程同时调用 signal() 或 fenv.h 函数,也不会损坏数据。 但要注意,调用影响所有线程,而不是只影响调用线程。

clearerr(), fclose(),

feof(),ferror(), fflush(),

fgetc(),fgetpos(), fgets(),

fopen(),fputc(), fputs(),

fread(),freopen(), fseek(),

fsetpos(),ftell(), fwrite(),

getc(),getchar(), gets(),

perror(),putc(), putchar(),

puts(),rewind(), setbuf(),

setvbuf(),tmpfile(), tmpnam(),

ungetc() 

如果实现了 _mutex_* 函数,则 stdio 库是线程安全的。

每个单独的流都使用锁进行保护,因此,两个线程可以分别打开并使用其自己的 stdio 流,而不会相互干扰。

如果两个线程都要读取或写入相同的流,fgetc() 和 fputc() 级别的锁定可防止发生数据损坏,但是,每个线程的单独字符输出可能会交叉出现,因而容易造成混淆。

请注意,tmpnam() 也包含一个静态缓冲区,但仅在自变量为 NULL 时才使用它。 要确保 tmpnam() 使用是线程安全的,应提供您自己的缓冲区空间。

  fprintf(), printf(),

vfprintf(), vprintf(), fscanf(),

scanf() 

使用这些函数时:

 

标准 C printf() 和 scanf() 函数使用 stdio,因而是线程安全的。

 

如果在多线程程序中调用标准 C printf(),其语言环境可能会发生变化。

clock() clock() 包含程序静态数据,此数据是在启动时一次性写入的,以后只能对其进行读取。 因此,clock() 是线程安全的,但前提是在初始化库时没有运行任何其他线程。 

errno() 

errno 是线程安全的。

 

每个线程将其自己的 errno 存储在 __user_perthread_libspace 块中。 这意味着,每个线程可以单独调用 errno 设置函数,然后检查 errno,而不用担心受其他线程的影响。

atexit() 

atexit() 维护的退出函数列表是进程全局性的,并且使用锁对其进行保护。

 

在最坏的情况下,如果多个线程调用 atexit(),则不能保证调用退出函数的顺序。

abs(), acos(), asin(),atan(),

atan2(), atof(),atol(), atoi(),

bsearch(),ceil(), cos(),

cosh(),difftime(), div(),

exp(),fabs(), floor(),

fmod(),frexp(), labs(),

ldexp(),ldiv(), log(),

log10(),memchr(), memcmp(),

memcpy(),memmove(), memset(),

mktime(),modf(), pow(),

qsort(),sin(), sinh(),

sqrt(),strcat(), strchr(),

strcmp(),strcpy(), strcspn(),

strlcat(),strlcpy(), strlen(),

strncat(),strncmp(), strncpy(),

strpbrk(),strrchr(), strspn(),

strstr(),strxfrm(), tan(), tanh() 

这些函数在本质上就是线程安全的。

longjmp(), setjmp() 

虽然 setjmp() 和 longjmp() 在 __user_libspace 中保存数据,但它们均调用线程安全的 __alloca_* 函数。

remove(), rename(), time() 

这些函数使用中断,以便与 ARM 调试环境进行通信。 通常,必须为实际应用程序重新实现这些函数。

snprintf(), sprintf(),

vsnprintf(),vsprintf(), sscanf(),

isalnum(),isalpha(), iscntrl(),

isdigit(),isgraph(), islower(),

isprint(),ispunct(), isspace(),

isupper(),isxdigit(), tolower(),

toupper(),strcoll(), strtod(),

strtol(),strtoul(), strftime() 

使用这些函数时,这些基于字符串的函数将读取语言环境。 通常,它们是线程安全的。 但是,如果在会话中更改语言环境,则必须确保这些函数不受影响。

 

基于字符串的函数并不依赖于 stdio 库,例如,sprintf() 和 sscanf()。

stdin, stdout, stderr 这些函数是线程安全的。

 

 

FP 状态字

 

可以在多线程环境(甚至软件浮点)中安全地使用 FP 状态字。 其中,每个线程的状态字存储在其自己的 __user_perthread_libspace 块中。

Note

 

请注意,在硬件浮点中,FP 状态字存储在 VFP 寄存器中。 在这种情况下,线程切换机制必须为每个线程保留该寄存器的单独副本。

非线程安全的函数

 

setlocale() 

语言环境设置是所有线程的全局设置,并且未使用锁对其进行保护。 如果两个线程调用 setlocale(),则可能会发生数据损坏。 另外,很多其他函数读取当前语言环境设置,例如,strtod() 和 sprintf()。 因此,如果一个线程调用 setlocale(),另一个线程同时调用此函数,则可能会产生意外结果。

 

ARM 建议您选择所需的语言环境,然后调用一次 setlocale() 以对其进行初始化。 应在程序中创建任何其他线程之前执行此操作,以使任意数量的线程可以同时读取语言环境设置,而不会相互干扰。

 

请注意,localeconv() 不是线程安全的。 应改用指向用户提供的缓冲区的指针调用 ARM 函数 _get_lconv()。

asctime(), localtime(),

strtok() 

这些函数不是线程安全的。 每个函数都包含一个静态缓冲区,其他线程可能会在调用函数以及随后使用其返回值之间覆盖该缓冲区。

 

ARM 提供了可重入版本 _asctime_r()、_localtime_r() 和 _strtok_r()。 ARM 建议您改用这些函数以确保安全。

Note

 

这些可重入版本使用一些附加参数。_asctime_r() 使用的附加参数是指向输出字符串要写入的缓冲区的指针。_localtime_r() 使用的附加参数是指向结果要写入的 struct tm 的指针。_strtok_r() 使用的附加参数也是一个指针,指向的是指向下一个标记的 char 指针。

gamma()[1], lgamma() 这些扩展 mathlib 函数使用全局变量 _signgam,因此不是线程安全的。

mbrlen(), mbsrtowcs(),

mbrtowc(),wcrtomb(),

wcsrtombs() 

stdlib.h 中定义的 C89 多字节转换函数(如 mblen() 和 mbtowc())不是线程安全的,因为它们包含在所有线程之间共享而没有锁定的内部静态状态。

 

但是,wchar.h 中定义的扩展可重启版本(例如,mbrtowc() 和 wcrtomb())是线程安全的,但前提是您传入指向您自己的 mbstate_t 对象的指针。 如果要在处理多字节字符串时确保线程安全,这些函数只能使用非 NULL 的 mbstate_t * 参数。

exit() 

即使提供了基本 _sys_exit()(实际终止所有线程)的实现,也不要在多线程程序中调用 exit()。

 

在这种情况下,exit() 在调用 _sys_exit() 之前 先执行清除操作,因此会中断其他线程。

rand(), srand() 

这些函数保留全局性且不受保护的内部状态。 这意味着,rand() 调用从来都不是线程安全的。

 

ARM 建议您使用自己的锁定,以确保每次只有一个线程调用 rand(),例如,通过定义 $Sub$$rand()(如果要避免更改代码)。

 

或者,也可以执行以下操作之一:

 

提供您自己的随机数生成器,它可能具有多个独立实例

 

硬性规定只有一个线程需要生成随机数。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值