final理解

Aug 5, 2016


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):

final_int

这就是类init方法(就是类构造方法中)的操作字节码

我们可以知道intVar_final常量初始化只能在构造方法中进行,并且只能初始化一次;

所以intVar_final一定是线程安全的

  • 1.因为intVar_final的状态只有一个并且不会发生改变
  • 2.类没有发生this指针逃逸

而普通变量intVar却会产生状态不一致的情况(具体如下图):

value_final

为什么会出现这种情况?

在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();
		// }
	}

}

执行结果如下图:

refer_final

为什么会出现:

Thread-254, referVar_final values: 1

因为线程A开始构造ReferenceVar_final实例对象之后,线程C开始读referVar_final引用的对象状态,此时对象初始化完成得到值1

而线程B进行referVar_final引用对象的改变,此时改变的值并没有被C所获取

referfinal

所以导致referVar_final对象状态不一致的问题,referVar_final对象是线程不安全的;

详情参考:

这几天状态不怎么好,见谅…

这篇博文只是谈到final一部分(发现脑CPU严重不足啊!),具体想深究可以去阅读这两本书