线程安全性
我们总是说要编写线程安全的代码,有时候也会讨论某个类是不是线程安全的。那到底什么是线程安全性呢?
网上有很多说法:可以被多个线程调用,并且线程之间不会出现错误的交互;
多个线程调用时,不需要做额外的动作等等。
但这话,明明什么都说了,又好像什么都没有说。到底怎么才能在多个线程之间安全地调用呢,怎么算安全呢?
正确性
在线程安全性定义中,最核心的概念就是正确性。如果对线程安全性的一定是模糊的,那么就是缺乏对正确性的清晰定义。
正确性的含义是:这个类的行为与其规范完全一致。我们通常不会为一个类编写详细的规范,但是这并不妨碍我们对正确性的理解,在写一个类、一个方法的时候,对于这个类的作用,这个方法的执行我们是有一个预期结果的。如果在多线程环境下,多个线程都在调用的时候,这个类这个方法仍然能始终保持预期的行为与结果,就是符合正确性的。在对正确性有一个比较清楚的认知之后,就可以定义线程安全性了:
当多个线程访问某个类时,不管这些线程如何交替执行,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的
对共享和可变的状态访问操作进行管理
那么如何做才能让类在多线程的环境下始终表现正确呢,换句话说,多线程环境下如何避免类出现不可预知,预料之外的行为。核心在于:要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。
对象的状态指的是存储在状态变量(例如实例或者静态域、成员变量)中的数据。对象的状态可能包含依赖的其它对象的状态。例如HashMap的状态不仅仅存储在HashMap本身,还存储许多Map.Entry对象中。在对象的状态中包含了任何可能影响其外部可见行为的数据。
共享意味着可以由多个线程同时访问、可变则意味这变量的值在整个生命周期中可以发生改变。当多个线程访问某个状态变量,并且至少有一个线程会修改变量的值时,就需要采用同步机制来协调这些线程对对象的访问。这里说的”同步机制“包括:synchronized关键字、volatile变量、显式锁、ThreadLoacl变量以及原子变量。
当多个线程同时访问一个可变的状态变量时,如果没有采用合适的同步,程序会出现错误。有3中方式可以解决这个问题。
1.不在线程之间共享该状态变量
2. 将状态变量修改为不可变变量
3.在变量访问时采用合适的同步
原子性、可见性、有序性
经常看到一种说法,叫”多线程的三个特性:原子性、可见性、有序性“。什么意思?多线程跟这三个东西有什么关系?
按照我的理解,原子性、可见性和有序性是代码执行的特征,为什么许多单线程下正常运行的代码跑的好好的,一旦多个线程同时访问就会出现莫名其妙的问题!恰恰是因为多线程破坏了这3个规则,在单线程环境下i++操作是原子的,但是到了多线程访问时,它就不是原子性了。多线程编程时我们为什么要加锁,要给变量加volatile关键词,要采取各种同步机制,就是需要保证不破坏原子性、可见性,保证有序性。
原子性:
一个操作或者多个操作,要么全部执行并且执行过程中不会被任何因素打断,要么不执行。
- 竞态条件
当某个计算的正确性,取决与多个线程交替执行的时序时,就会发生竞态条件。典型的竞态条件就是:先检查后更新,本质上是:基于一种可能失效的观察结果来做出判断或者执行操作
例如我们常见的 i++操作,其实包含了3个操作。先获取i的值,将i的值加1,将加1后的值写入变量i。
如果没有进行合适的同步(将上面3个操作整合为原子操作),多个线程同时执行会得到不符合预期的结果:可能两个线程都得到2。
2.复合操作
有时候我们需要一组操作以原子方式执行,在执行过程中要防止其它线程使用这个变量。只能允许其它线程在复合操作执行前或者执行后访问状态变量,而不是在修改的过程中。
可见性
多个线程同时访问一个共享变量时,某个线程修改了共享变量,其它线程能够立即获取到修改后的值
有序性
程序的执行顺序按照代码的先后顺序执行
由于指令重排等虚拟机优化策略,可能会导致程序的执行顺序并不严格按照代码的顺序。
多线程就是需要保证这3个特性的正常执行。