线程安全问题
非线程安全主要指多个线程对同一个对象的实例变量进行操作时,值不同步的情况。
线程安全问题表现为三个方面:原子性,可见性和有序性.
原子性
原子(Atomic)指不可分割的意思,主要有两层含义
1.线程访问某个共享变量的操作从其他线程来看,该操作要么执行完毕,要么尚未执行。其他线程看不到当前线程的中间结果。
2.访问同一组共享变量的原子操作是不能够交叉的。
eg:ATM取钱:要么操作成功取到钱,要么操作失败没取到钱。
java中实现原子性的方式:一种是使用锁,另一种是使用处理器的CAS(Compare and Swap)指令。
锁具有排它性,保证共享变量在某一时刻只能被一个线程访问。
CAS指令直接在处理器和硬件层次上实现,看作是硬件锁。
代码示例:
结果:
问题原因:
步骤未执行完,另一个线程就开始执行。
解决方案:
可见性
在多线程环境中,一个线程对某个共享变量进行更新之后,后续其他的线程可能无法立即读到这个更新的结果,这就是线程安全的可见性问题。
代码示例
结果:
说明获取不到最近更新的flag的值,发生了线程不可见。
解决方案:
使用通过 volatile 解决,volatile字段主要用于线程之间进行通信,volatile字段的每次读行为都能看到其它线程最后一次对该字段的写行为,所以共享变量的数据是最新的。
有序性
是指在一个处理器上运行的一个线程所执行的内存访问操作在另一个处理器运行的其他线程看来是乱序的。
乱序:是指内存访问操作顺序看起来发生了变化。
int i = 0; boolean flag = false; i = 1; //语句1 flag = true; //语句2
从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)
指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同。那么它靠什么保证的呢?再看下面一个例子:
int a = 10; //语句1 int r = 2; //语句2 a = a + 3; //语句3 r = a*a; //语句4
这段代码有4个语句,那么可能的一个执行顺序是:
那么可不可能是这个执行顺序呢: 语句2--->语句1 ---> 语句4--->语句3
不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
//线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。