线程安全
什么是线程安全
程序运行的结果100%符合预期,不会出现有时正确,有时错误。 ——消除随机性并且正确。
什么时候会出现线程不安全
1.看线程之间是否有共享的数据的。
2.有线程修改共享数据。
上述两点都满足的话,则会出现线程不安全。
换言之,1.线程之间不存在共享的,天生是线程安全的。
2,即使线程之间有数据共享,但没有线程修改共享数据,则天生是线程安全的。
JVM运行时内存区域中,哪些位置是共享的?哪些是线程内部私有的?
1.nextPC值(PC区域)—— 私有的
2.栈区(java栈/native栈)——私有的
3.堆区 —— 共享的
4.方法区 —— 共享的
5.运行时常量池 —— 共享的
什么时候需要特别考虑线程安全问题
线程之间有修改静态属性或同一对象属性时,需要特别考虑线程安全问题,否则,一般不需要考虑。
局部变量是线程私有的数据,不会共享,不需要特别考虑线程安全问题。
属性/对象 + 静态属性/类 ,是共享的,需要考虑线程安全问题。
原子性、代码可见性、代码重排序
线程之间是以共享数据进行通信的,就需要特别关注这三点:原子性、代码可见性、代码重排序。
原子性
原子性是指一组不能再分割的操作。
例如:
float f = 1.0F —— 具备原子性
double a = 1.0 ——不具备原子性
(注意:JVM在设计时,是按照32bit为最小操作单位设计的)
double类型是64bit的,double a = 1.0 可被分为高32bit赋值 和 低32bit赋值,是两步操作,所以无法保证它的原子性。
int a = 9;
int b = a; ——也不是原子性的。它有两步操作:1.先从内存中读取a的值 2.把寄存器中的值
保存到b的内存中。是两步操作。
final int a = 7;
int b;
b= a; ——具备原子性。因为a是常量,就不需要读取a这步操作。
代码可见性
代码可见性指一个线程修改共享变量时,另一个线程是否能看到的问题。
java内存模型规定,所有的线程都有自己的工作内存。线程需要操作任何数据都需要把数据从主内存中加载到工作内存中,线程就在自己的工作内存中操作,在合适的机会,再把计算结果同步到主内存中。如果线程操作变量后没有将结果同步到主内存,那么操作对其他线程是不可见的。如果一个变量没有读取到新的值而是使用旧的值,也是不可见的。
代码重排序
定义:程序执行顺序和语句顺序不一致,这时,便发生了代码重排序。
作用: 重排序后执行效率更高。
谁来做代码重排序: 编译器 和 处理器。
特征:
1.单线程情况下,代码重排序后的结果和重排序之前效果一致。
2.多线程情况下,重排序后结果和重排序前结果不一致。
要求 JVM规定:重排序必须遵守happend-before 原则——规定哪些语句必须哪些语句之前。
要防止重排序,需要使用volatile关键字,它可以保证操作不会被重排序。