线程的使用一直是难以把控掌握的。如果使用得当,线程可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。在GUI应用程序中,提高用户界面的响应灵敏度,在服务器应用程序中,提升资源利用率以及系统吞吐量。
然而,如果使用不当,线程将会带来一系列不可预估的风险。Java对线程的支持其实就是一把双刃剑。虽然Java明确是一种跨平台(编写异常,随处运行)的语言,JDK并提供了相应的类库,简化了程序的开发,但更多的在处理一些复杂程序时,就需要使用线程,随之引入的“并发性”问题(线程安全性),就成为了开发人员实现考虑的难点。
在多线程中的各个操作的顺序都是不可预测的,有时其执行结果简直出乎意料,令人惊讶。例如,通过下面代码片段的数值序列生成器,用来生成一个递增序列。
package com.xcbeyond.thread;
import net.jcip.annotations.NotThreadSafe;
/**
* 非线程安全的数值序列生成器
* @author xcbeyond
* 2018-5-6下午03:17:33
*/
public class UnSafeThreadSequence {
private int value;
/**
* 返回一个唯一的值
* @return
*/
public int getValue() {
return value++;
}
}
如果在单线程中执行,没有任何问题,结果也正如我们预期的一样。而在多线程并发的场景下操作,将会出现不同线程之间交替操作,不同线程很可能同时读取同一个值,得到相同值,导致不同线程返回了相同的序列值,这恰恰与我们的预期截然相反。
这充分说明了一种常见的线程并发危险:竞争条件。因为线程共享了相同的内存空间地址,且并发的执行,它们可能访问或修改其他线程正在使用的变量,将会出现变量竞争情况。
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
如何编写线程安全的代码呢?
编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。一个对象是否需要时线程安全的,取决于该对象是否被多线程访问。这指的是程序中访问对象的方式,而不是对象要实现的功能。要使得对象是线程安全的,要采用同步机制来协同对对象可变状态的访问。Java常用的同步机制是Synchronized,还包括 volatile类型的变量,显示锁以及原子变量。
原子性:
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B完全执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指:对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
竞态条件(Race Condition):某个计算/程序的正确性取决于多个线程的交替执行的时序。(线程的时序不同,产生的结果可能会不同)
“先检查后执行”,即通过一个潜在的过渡值来决定下一步的操作。
首先观察到某个条件为真,然后开始执行相关的程序,但是在多线程的运行环境中,条件判断的结果以及开始执行程序中间,观察结果可能变得无效(另外一个线程在此期间执行了相关的动作),从而导致无效。
“读取-修改-写入”,基于对象之前的状态来定义对象状态的转换。即使是volatile修饰的变量,在多线程的环境里面进行自增操作,同样会发生竞态条件,所以volatile不能保证绝对的线程安全。
加锁机制:
更多详见:http://www.mark-to-win.com/tutorial/50306.html