目录
问题的提出
思路 - 同步锁
思路 - 不可变
不可变设计
final 的使用
保护性拷贝
设置 final 变量的原理
获取final变量的原理
日 期 转 换 的 问 题
public class ThreadText {
public static void main(String[] args) throws InterruptedException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(sdf.parse("1951-04-21"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果,例如:
上述代码会出现线程安全问题,因为SimpleDateFormat对象并不是线程安全的,即多个线程同时调用SimpleDateFormat实例的同一个方法时可能会产生冲突。
在多线程环境下,如果多个线程同时访问SimpleDateFormat对象的同一个方法,那么会出现问题:
线程安全问题:SimpleDateFormat是非线程安全的类,如果多个线程同时访问,可能会导致解析错误或者计算结果混乱
这样虽能解决问题,但带来的是性能上的损失,并不算很好:
public class ThreadText {
public static void main(String[] args) throws InterruptedException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 50; i++) {
new Thread(() -> {
synchronized (sdf) {
try {
System.out.println(sdf.parse("1951-04-21"));
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:
public class ThreadText {
public static void main(String[] args) throws InterruptedException {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LocalDate date = dtf.parse("2023-06-15", LocalDate::from);
System.out.println(date);
}).start();
}
}
}
不可变对象,实际是另一种避免竞争的方式。
另一个大家更为熟悉的 String 类也是不可变的,以它为例,说明一下不可变设计的要素
public final class String
implements java.io.Serializable, Comparable, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
// ...
}
发现该类、类中所有属性都是 final 的 属性用 final 修饰保证了该属性是只读的,不能修改 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是 如何实现的,就以 substring 为例:
发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出 了修改:
结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避 免共享的手段称之为【保护性拷贝(defensive copy)】
public class TestFinal {
final int a = 20;
}
字节码
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I
<-- 写屏障
10: return
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况
首先,final变量会被显式初始化或在构造函数中初始化。在编译期间,final变量的值就已经确定并被存储在常量池中,而不是在运行时通过初始化代码计算得到。
因为final变量的值已经确定,Java虚拟机在读取final变量的值时,会直接从常量池中读取,而不是从堆内存中读取。因此,获取final变量的过程可以看作是一个常量折叠过程:编译器在编译期间把所有引用final变量的地方替换成该变量的值。
这种优化方式的好处在于可以加快程序的执行速度,同时也可以避免线程安全问题,因为final变量的值不可修改,也不需要进行同步处理。
除此之外,final变量也具有内存可见性,即使在多线程环境下,其值也能够保证对其他线程是可见的。这是因为,当一个线程将final变量的值写入主内存后,其他线程读取该变量时,会从主内存加载该变量的值,而不是从自己线程内部的缓存中加载,从而保证了线程之间final变量值的可见性。
综上,获取final变量的原理是通过常量池来实现的,其值在编译期间被确定并存储在常量池中。这种方式具有较高的执行效率和线程安全性,同时也保证了final变量值在多线程环境下的可见性。