Thread线程安全
要理解线程安全我们首先举一个例子:
问题:
-
启动两个线程,同时操作一个变量 v = 0
-
一个线程对该变量执行 N 次 v++
-
另一个线程对该变量执行 N 次 v–
-
问,当两个线程都执行结束时,v 的值是多少?
结论:
理论上我们的期望值应该是0,但是实际上是一个随机值(也有可能出现0),并且当N越大时出现随机值的概率越高。
通过这个例子我们就可以简单的给线程安全下一个定义,程序运行的结果假如我们100%符合我们的预期,不会出现有时正确有时错误,就是线程安全。
再回到我们刚才那个例子,为什么会产生这样的结果?我们先要理解非常重要的几个概念。
1.java中的一条语句,对应的不一定是一条字节码,更不一定是一条cpu指令。所以我们模拟刚才的v++过程产生的指令;实际上可能更加复杂。
v++; 1.加载变量到cpu的寄存器上 。2.让cpu把寄存器上的值+1。3.把计算的v值再返回给内存上。
2.线程调度具有随机性存在,什么时候cpu调度下来,什么时候cpu调度回去,我们并不知道。Add和Sub的流程是
Add
1.把v的值加载在register
2.计算+1
3.把结果保存回v
Sub
1.把v的值加载在register
2.计算-1
3.把结果保存回v
如果发生调度是这样的,那么本来是一次++,一次–,值本来是0,但是由于调度原因,v–的值被覆盖了。所以只能看到++,所以此时的v的值就是1。这就是产生随机值得原因。N越大,发生这种情况的机会越大,当然这调度的一种情况,还有多种情况,但是都道理相同。
那么什么情况下会出现线程不安全?
答:共享+修改。
也就是说出现线程不安全前提是线程之间是共享一份数据的,并且修改了这个共享数据。
紧接着我们需要了解java JVM虚拟机运行时内存区域中,那些位置的数据是共享的,那些是线程内部私有的。(图片转载)
局部变量——>栈帧@栈 不共享
对象的属性——>对象@堆 共享
类的静态属性——>类@方法区 共享
局部变量是私有的,不需要考虑线程安全问题。属性/对象,静态属性/类是共享的需要考虑安全问题。
考虑线程不安全时,有三点需要我们特点关心。1.原子性 2.内存可见性 3.代码重排序。
分别介绍:
- 原子性(atomic)——来源之前科学界认为原子不可再分
java中 一组不能再被分割的操作,被称为保证原子性。
关于一些赋值时的原子性。jvm设计时是按照32bit设置的,比如
int i =1;
short s=1;
byte b=1; 都是具备原子性的
long l=1; 是不具备原子性的。原因是这次赋值操作必须被分解为高32位赋值+低32位赋值。
八种基本类型的bit位。
2.内存可见性
线程内存与主内存图示内存可见性问题:因为高速缓存具有隔离性(因为线程有自己的工作内存,线程要操作数据,先把数据从主内存加载到工作内存中,等到合适的机会再把在自己工作内存的结果同步到主内存)所以主内存的值不是最新的结果。
3.代码重排序
int a=1;
int b=1;
int c=1;在单线程情况下,要代码重排序必须要保证结果一致。
多线程情况下,代码重排序就可能导致线程安全问题。