@@ -9,6 +9,7 @@ final能够修饰变量,方法和类,也就是final使用范围基本涵盖
9
9
10
10
11
11
![ final修饰成员变量] ( http://upload-images.jianshu.io/upload_images/2615789-768017317b5fab78.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
12
+
12
13
看上面的图片已经将每种情况整理出来了,这里用截图的方式也是觉得在IDE出现红色出错的标记更能清晰的说明情况。现在我们来将这几种情况归纳整理一下:
13
14
14
15
1 . ** 类变量** :必须要在** 静态初始化块** 中指定初始值或者** 声明该类变量时** 指定初始值,而且只能在这** 两个地方** 之一进行指定;
@@ -17,6 +18,7 @@ final能够修饰变量,方法和类,也就是final使用范围基本涵盖
17
18
final局部变量由程序员进行显式初始化,如果final局部变量已经进行了初始化则后面就不能再次进行更改,如果final变量未进行初始化,可以进行赋值,** 当且仅有一次** 赋值,一旦赋值之后再次赋值就会出错。下面用具体的代码演示final局部变量的情况:
18
19
19
20
![ final修饰局部变量] ( http://upload-images.jianshu.io/upload_images/2615789-7077bdb169d4d1c3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
21
+
20
22
现在我们来换一个角度进行考虑,final修饰的是基本数据类型和引用类型有区别吗?
21
23
22
24
> ** final基本数据类型 VS final引用数据类型**
@@ -104,6 +106,7 @@ final局部变量由程序员进行显式初始化,如果final局部变量已
104
106
父类会被final修饰,当子类继承该父类的时候,就会报错,如下图:
105
107
106
108
![ final类不能继承] ( http://upload-images.jianshu.io/upload_images/2615789-835b66d960e21e2e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
109
+
107
110
# 3. final的例子 #
108
111
final经常会被用作不变类上,利用final的不可更改性。我们先来看看什么是不变类。
109
112
> 不变类
@@ -168,6 +171,7 @@ JDK中提供的八个包装类和String类都是不可变类,我们来看看St
168
171
我们来画下存在的一种可能执行时序图,如下:
169
172
170
173
![ final域写可能的存在的执行时序] ( http://upload-images.jianshu.io/upload_images/2615789-9e3937df955a9862.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800 )
174
+
171
175
由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。
172
176
173
177
因此,写final域的重排序规则可以确保:** 在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障** 。比如在上例,线程B有可能就是一个未正确初始化的对象finalDemo。
@@ -187,6 +191,7 @@ read()方法主要包含了三个操作:
187
191
188
192
189
193
![ final域读可能存在的执行时序] ( http://upload-images.jianshu.io/upload_images/2615789-2a93b67948d7fc64.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800 )
194
+
190
195
读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。
191
196
192
197
读final域的重排序规则可以确保:** 在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。**
@@ -281,6 +286,7 @@ JMM可以确保线程C至少能看到写线程A对final引用的对象的成员
281
286
可能的执行时序如图所示:
282
287
283
288
![ final域引用可能的执行时序] ( http://upload-images.jianshu.io/upload_images/2615789-e020492056ee1242.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
289
+
284
290
假设一个线程A执行writer方法另一个线程执行reader方法。因为构造函数中操作1和2之间没有数据依赖性,1和2可以重排序,先执行了2,这个时候引用对象referenceDemo是个没有完全初始化的对象,而当线程B去读取该对象时就会出错。尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,其final域已经完全初始化成功。但是,引用对象“this”逸出,该代码依然存在线程安全的问题。
285
291
286
292
0 commit comments