final如何保证线程安全?
final声明的变量一定是线程安全的吗?
不可变对象
什么是不可变对象?
意味着对象在整个程序生命周期内其状态是不可改变的:
状态 —> 对象的属性
在Java并发中对不可变对象的定义:
- 1.对象创建后,状态不可被改变
- 2.对象的属性是final类型
- 3.在对象实例化的过程中没有发生this指针逃逸dymdmy2120.github.io
不可变对象只有一个状态,并且该状态由类的构造方法所控制(静态常量状态则是由静态块控制);
不可变对象一定是线程安全的
比如String类,感兴趣可以去看JDK中String源码,在此不做深究;
final类型
在上面讲到,不可变对象一定是线程安全的,不可变对象中有一个规则就是对象的属性是final类型;
那这样是否意味着final类型的变量就一定是线程安全?
在Java中变量类型分为:
- 值类型
- 引用类型
若不清楚值类型,引用类型的区别请自行查阅资料
final的作用就是标明变量的状态不可变
final—值类型
1.如果使用final声明值类型的变量,变量初始化之后其值不可修改(初始化指的是在对象构造方法中进行赋值初始化操作);
public class BasicVar_final {
final int intVar_final; //final int intVar_final = 4;初始化方式:(1)
static BasicVar_final instance;
int intVar = 1;
public BasicVar_final() {
System.out.println("constructer...");
intVar = 2;
intVar_final = 2;//final值类型初始化方式:(2)
}
public static void writerInstance() {
instance = new BasicVar_final();
}
public static void readIntVar() {
BasicVar_final obj = instance;
System.out.println(Thread.currentThread().getName()
+ ", intVar_final value: " +
obj.intVar_final);
System.out.println("-------------------");
System.out.println(Thread.currentThread().getName()
+ ", intVar value: " + obj.intVar);
System.out.println();
}
}
我们先只看intVar_final常量初始化的方式:
- final int intVar_final = 2; //直接在定义常量的时候进行赋值初始化操作;
- inVar_final = 2 //直接在构造方法中进行赋值初始化操作;
其实两种方式的本质都是一样,都是在类的实例方法(init方法)中进行初始化操作;
注意:声明的常量只能初始化一次,即常量初始化之后在任何地方对常量进行赋值操作编译器都将报错;
我们查看该类的字节码文件来进行分析(javap -verbose BasicVar_final):
这就是类init方法(就是类构造方法中)的操作字节码
我们可以知道intVar_final常量初始化只能在构造方法中进行,并且只能初始化一次;
所以intVar_final一定是线程安全的
- 1.因为intVar_final的状态只有一个并且不会发生改变
- 2.类没有发生this指针逃逸
而普通变量intVar却会产生状态不一致的情况(具体如下图):
为什么会出现这种情况?
在Java内存模型中对程序代码编译之后有一个重排序的一个处理过程
重排序对于我的理解就是JVM会对你规定顺序编写的代码做处理,使代码不会顺序执行
由上图我们可以知道,intVar初始化的操作被重排序在构造方法之外,这就导致intVar变量的状态不一致的问题;
为什么intVar_final不会出现这个问题呢?
在JMM(Java内存模型)中对final声明的变量重排序有两个规则:
- 1.final变量不能重排序在构造方法之外
- 2.在第一次访问包含final域对象引用和第一次访问final域变量不能发生重排序(需先访问对象引用,再访问final变量)
final—引用类型
引用类型在栈中标明为reference
引用类型指向的是对象的内存地址,一个引用变量声明为final代表其引用不可变
意思就是这个引用变量不可以再指向其他对象的内存地址
但是引用本身指向的对象状态是可以改变的
所以用final声明引用类型有以下结论:
- 1.引用不可变
- 2.引用指向的对象状态可变
- 3.引用类型声明为final存在线程安全问题
举个例子来证明:
public class ReferenceVar_final {
final int[] referVar_final;
static ReferenceVar_final instance;
public ReferenceVar_final() {
//对referVar_final引用进行初始化操作;
referVar_final = new int[1];
referVar_final[0] = 1;
}
//对instance对象进行实例化
public static void writerInstance() {
instance = new ReferenceVar_final();
}
//改变referVar_final对象状态
public static void writerReferVar() {
instance.referVar_final[0] = 2;
}
//读取referVar_final状态
public static void reader() {
if (instance != null)
System.out.println(Thread.currentThread().getName()
+ ", referVar_final values: " + instance.referVar_final[0]);
}
public static void main(String[] args) {
//int i = 0;
// while (i++ <= 100) {
//线程A,进行instance对象的写入
new Thread(() -> {
ReferenceVar_final.writerInstance();
}).start();
//线程B,进行referVar_final对象状态的改变
new Thread(() -> {
ReferenceVar_final.writerReferVar();
}).start();
//线程C,进行referVar_final对象状态的读取
new Thread(() -> {
ReferenceVar_final.reader();
}).start();
// }
}
}
执行结果如下图:
为什么会出现:
Thread-254, referVar_final values: 1
因为线程A开始构造ReferenceVar_final实例对象之后,线程C开始读referVar_final引用的对象状态,此时对象初始化完成得到值1
而线程B进行referVar_final引用对象的改变,此时改变的值并没有被C所获取
所以导致referVar_final对象状态不一致的问题,referVar_final对象是线程不安全的;
详情参考:
这几天状态不怎么好,见谅…
这篇博文只是谈到final一部分(发现脑CPU严重不足啊!),具体想深究可以去阅读这两本书