线程安全是指单线程情况下运行正常,在多线程环境下,不需要做任何改变的情况下也能正常运行,称为线程安全,否则称为线程不安全。线程不安全下,多线程下会出现竞态。
竞态
竞态是一种现象,多线程编程中经常遇到一个问题就是对于相同的输入,程序的输出有时候正确,有时候错误,这个现象称为竞态。一般发生竞态条件是存在以下两种操作情况:
<center>check-then-act和read-modify-write</center>
check-then-act:先获取共享变量的值,根据共享变量的值来觉得下一步动作。
read-modify-write:先获取共享变量的值,对其进行修改,然后把修改后的共享变量写会内存。
当一个类不是线程安全的时候,那么需要考虑他在多线程情况的线程安全问题。线程安全主要有以下三个方面:
原子性
原子性的含义主要包括两个方面:1对某个变量的操作,在其执行外的线程看,该操作要么执行完成,要么没有发生,不会看到中间结果。 2访问一组共享变量的原子操作,不会和其他线程交错执行。
java保证原子性一般采用1.锁(lock和synchronized)2 CAS。期中锁是通过软件来保证 ,CAS是通过硬件来保证。在java规范中:对于基础类型(非long/double)byte,boolean,short,char,float,int变量和引用变量的写是原子的,对于long/double类型的变量不强制原子,但是必须保证volitale修饰的long和double的操作是原子,在64位jvm中,比如OpenJDK 7/8,实现了对long的原子操作。java9中提供了+AlwaysAtomicAccesses参数来以实现对所有Access的原子性保证,但是对于32位的虚拟机仍然无法保证。double因为一般cpu都有专门的浮点单元,其存取哪怕是在32bit jvm上一般都是原子的。
可见性
多线程环境下,一个线程对共享变量更新之后,后序线程可能无法立即读取到更新后的结果,或者永远都无法读取这个更新结果,这就是多线程直接的可见性问题。由于计算机存储系统是多级存储,cpu访问的数据,不是直接从内存获取,cpu更新计算后的数据也不是直接写入到内存中。在cpu每个内核和内存中间有高速缓存(cache),并且每个cpu内核内部还有寄存器,每个cpu通常是从寄存器里读取数据,因此不同cpu直接是无法直接读取彼此寄存器和缓存的值。不同线程在不同cpu上执行,那么一个线程对共享变量进行更新后,新值保存到cpu的写缓冲器,还没有到达cpu的高速缓存中,更不用说内存了。而执行在另一个cpu上的线程是无法获取这个共享变量的更新。因此导致了多线程的可见性问题。通常计算机采用缓存一致性协议来保证多cpu之间的数据可见性。java中可以使用volitale和synchronized可以保证共享变量的可见性。
有序性
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的。但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。
重排
重排是对内存访问有关操作(读/写)的优化,对于单线程来说,重排必须保证重排之后仍能和重排之前有相同的执行结果。包括:指令重排和存储系统重排:
- 指令重排发生在编译器,处理和JIT编译器:它指cpu执行的指令代码与源码顺序不一致。指令重排发生在指令的执行过程中
- 子存储系统重复(写缓冲器和高速缓存):发生在高速缓存和写缓冲器,它指其他cpu感知顺序与执行顺序不一致。由于cpu对共享变量更新后,会写入子存储系统中,然后通过子存储系统写回内存。虽然程序没有发生指令重排,但是在子存储系统往内存写数据的时候,可能不一定按照cpu往子存储系统写入的顺序去执行。其他cpu从内存中读取共享变量的时候,也不一定会安装程序执行的顺序去从内存中加载其他cpu写入额共享变量值,因此其他cpu就会感知到共享变量的更新顺序与执行顺序不一致。
两个术语:读内存称为load,写内存称为store
java规定对于单线程来说,有两个原则:
1.有数据依赖关系的操作是不能进行重排
2.重排之后,对于执行结果要保证as-if-serial,就是无论如何重排,执行结果必须和没有重排的一致。
java中只能保证单线程的as-if-serial语义,那么多线程情况需要特殊的处理:比如使用volitale和synchronized来禁止重排
网友评论