谈到 Java 的多线程编程,一定绕不开线程的安全性,线程安全又包括原子性,可见性和有序性等特性。今天,我们就来看看他们之间的关联和实现原理。
线程与竞态
开发的应用程序会在一个进程中运行,换句话说进程就是程序的运行实例。运行一个 Java 程序的实质就是运行了一个 Java 虚拟机进程。
如果说一个进程可以包括多个线程,并且这些线程会共享进程中的资源。任何一段代码会运行在一个线程中,也运行在多个线程中。线程所要完成的计算被称为任务。
为了提高程序的效率,我们会生成多个任务一起工作,这种工作模式有可能是并行的,A 任务在执行的时候,B 任务也在执行。
如果多个任务(线程)在执行过程中,操作相同的资源(变量),这个资源(变量)被称为共享资源(变量)。
当多个线程同时对共享资源进行操作时,例如:写资源,就会出现竞态,它会导致被操作的资源在不同时间看到的结果不同。
来看看多个线程访问相同的资源/变量的例子如下:
![ba2acfc07e536b27a2334a43907bdd13.png](https://img-blog.csdnimg.cn/img_convert/ba2acfc07e536b27a2334a43907bdd13.png)
当线程 A 和 B 同时执行 Counter 对象中的 add() 方法时,在无法知道两个线程如何切换的情况下,JVM 会按照下面的顺序来执行代码:
- 从内存获取 this.count 的值放到寄存器。
- 将寄存器中的值增加 value。
- 将寄存器中的值写回内存。
上面操作在线程 A 和 B 交错执行时,会出现以下情况:
![d333ea44dd06fa57e1a71ccb5030690a.png](https://img-blog.csdnimg.cn/img_convert/d333ea44dd06fa57e1a71ccb5030690a.png)
两个线程分别加 2 和 3 到 count 变量上,我们希望的结果是,两个线程执行后 count 的值等于 5。
但是,两个线程交叉执行,即使两个线程从内存中读出的初始值都是 0,之后各自加了 2 和 3,并分别写回内存。
然而,最终的值并不是期望的 5,而是最后写回内存的那个线程(A 线程)的值(3)。
最后写回内存的是线程 A 所以结果是 3,但也有可能是线程 B 最后写回内存,所以结果是不可知的。
因此,如果没有采用同步机制,线程间的交叉写资源/变量,结果是不可控的。
我们把这种一个计算结果的正确性与时间有关的现象称作竞态(Race Condition)。
线程安全
前面我们谈到,当多线程同时写一个资源/变量的时候会出现竞态的情况。这种情况的发生会造成,最终结果的不确定性。
如果把这个被写的资源看成 Java 中的一个类的话,这个类不是线程安全的。
即便这个类在单线程环境下运作正常,但在多线程环境下就无法正常运行。例如:ArrayList,HashMap,SimpledateFormat。
那么,为了做到线程安全,需要从以下三个方面考虑,分别是:
- 原子性
- 可见性
- 有序性
原子性