原子性、可见性和有序性

»Java线程

目录:

原子性

原子性字面意思是不可分割的。对于涉及共享变量访问的操作,如果该操作对于其他线程来看是不可分割的,那么该操作就是原子操作,该操作就具有原子性。

所谓的”不可分割”,有两层意思:

  1. 访问某个共享变量的操作(读、写),从其他线程来看,要么没有执行,要么执行结束,即不可能看到该操作执行了一部分的中间结果。
  2. 原子操作是不能够被交错执行的。

原子性类似于”事务”一样。

同时原子操作是针对访问共享变量的操作而言的。仅涉及局部变量的访问操作无所谓原子性,单线程环境下的操作无所谓原子性,即我们都可以把这些都看成具有原子性。

JAVA实现原子性有两种方式:

  1. 使用锁(LOCK)
  2. 使用处理器专门提供的CAS指令。

锁通常是在软件层面实现的,而CAS是在硬件层面实现的。

同时,除了long和double之外的任何类型的变量的写操作都是原子性的,但是使用volatile修饰的long/double变量就具有原子性了。

除此之外,原子操作 + 原子操作 != 原子操作

可见性

在多线程下,一个线程对某个共享变量进行更新后,其他线程可能无法立刻获取更新后的新值。如果一个线程对某个共享变量进行更新之后,后续访问该变量的线程可以读取到该更新后的结果,那么我们就称这个线程对该变量的更新对其他线程是可见的。

造成可见性问题的问题与计算机的存储系统有关,处理器并不直接与主内存打交道,而是通过缓存将内存中的值拷贝到寄存器,然后进行计算,计算的结果也不是立即就更新到主存,并且并不是每次运算都会从主存中读取数据。

虽然处理器缓冲中的数据不能被其他处理器直接读取,但是可以通过缓存一致性协议来读取其他处理器高速缓存中的数据,并将读取的数据更新到该处理器的缓存中。这种一个处理器从其自身处理器缓存之外的其他存储部件中读取数据并将其更新到自己的缓存的过程称为缓存同步。

通常可以通过volatile关键字实现变量的可见性,

可见性的保障仅仅意味着一个线程能够读取到共享变量的相对新值,而不能保障该线程能够读取到最新值。例如有两个线程同时读取到一个被volatile修饰的变量,而其中一个线程立刻对该变量更新并写回主存,而另一个线程此时读取到的就是相对新值。

同时,父线程在子线程启动前对共享变量的更新对子线程是可见的。一个线程对共享变量的更新在其终止后对于调用该线程join方法的线程是可见的。

有序性

通常一个处理器上的线程执行的操作在其他处理器上的线程上看是乱序的。处理器会在不影响单线程程序正确性的情况下通过对内存访问有关的操作进行重排序来提升程序性能。

volatile、synchronized关键字都可以实现有序性。

内存屏障保障可见性和有序性

按照可见性划分,内存屏障可以分为加载屏障存储屏障。加载屏障的作用是刷新处理器缓存,存储屏障的作用是冲刷处理器缓存。

加载屏障使得读线程的执行处理器能够将写线程对相应共享变量所做的更新从其他处理器同步到该处理器的高速缓存中。存储屏障使得写线程对共享变量所做的更新对读线程的执行处理器来说是可同步的

按照有序性划分,内存屏障可以分为获取屏障释放屏障。获取屏障的使用方式是在一个读操作之后插入该屏障,作用是禁止读操作与后面的任何读写操作进行重排序。释放屏障的使用方式是在一个写操作之前插入该屏障,作用是禁止该写操作与之前的任何读写操作之间进行重排序。