如果一个对象再也不能够修改其内部状态(属性),那么即使它是共享的也是线程安全的,因为不存在并发修改。
**比如类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性**
问题演示:日期转换问题
下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的 , 很大几率出现 java.lang.NumberFormatException或者出现不正确的日期解析结果,因为SimpleDateFormat其内部的属性是可变的,多个线程的访问的情况下就有可能出现问题。例如:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
多次运行后就可能抛出如下问题
像这种可变类若果不对其进行线程安全保护的话就有可能产生错误,使用同步锁进行同步控制后虽然可以避免线程安全问题 但带来的是性能上的损失,并不算很好, 因为加锁耗性能 。@Slf4j(topic = "guizy.Test2")
public class Test2 {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (sdf) {
try {
log.debug("{}", sdf.parse("2020-12-29"));
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
另一种性能更优良的解决方法是 使用JD8中的 不可变日期格式化类 DateTimeFormatter ,它的实现是被final关键字修饰的,是不可变的类。
@Slf4j(topic = "guizy.Test2")
public class Test2 {
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
TemporalAccessor date = dtf.parse("2020-12-29");
log.debug("{}", date);
}).start();
}
}
}
不可变对象,实际是另一种避免竞争的方式。
不可变类的设计
final关键字的使用
- Integer、Double、String、DateTimeFormatter以及基本类型包装类, 都是使用final来修饰的
- 另一个大家更为熟悉的 String 类也是不可变的,这里我们以它为例说明一下不可变类设计的要素
部分源码:JDK1.8之后char数组变为了byte数组(与我们现在研究的问题无关)
查看该类的源码我们可以发现该类、类中所有属性都是 final 的,而属性一旦用 final 修饰保证了该属性是只读的,不能修改,类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性。
保护性拷贝
- 我们都知道final修饰数组只能保证它的引用不可改变,而不能保证数组里面的内容是不变的,那这String类又是如何处理这个问题的呢?
我们来看看String的构造方法,若果是传递一个原始字符串并根据它生成一个新的字符串的话,它会与原始的字符串共用这个value数组。
若果是传递过来一个char数组的话,它会把传递过来的数组进行拷贝成一个新数组,这个新数组就做为String的value数组;为什么要这样做呢?
因为如果不进行复制的话,可能外部还有一个char数组引用指向同一个对象,那么外部的char数组内元素改变了,就会导致String里面的char数组内容改变,进而破坏了String的不可变性。
正是这种复制的方式,保证了String字符串的不可变性。这种思想就叫做保护性拷贝。
但有人会说,使用字符串时,也有一些跟修改相关的方法啊,比如substring等,那么下面就看一看这些方法是如何实现的,substring部分源码:
可以看到该方法的最后一步返回的是:调用String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对final char[] value做出了修改:
结果发现也没有,构造新字符串对象时,会生成新的char[]value数组,对字符串内容进行复制。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】
享元模式
虽然保护性拷贝能够保证不可变性,避免共享保证线程安全;但也会产生一个问题:对象的创建太频繁了,所以为了解决这个问题,对于这种不可变类都会去关联一个享元设计模式(Java设计模式)。
该模式的英文简介定义名称为:Flyweight pattern, 一般用在
**重用数量有限的同一类对象**
的场景下。 比如说字符串的保护性拷贝时,都要创建新对象的话,若是有取值相同的我们就可以重用这些对象。享元模式的体现
包装类
在JDK中Boolean,Byte,Short,Integer,Long,Character等包装类提供了valueOf方法。
例如 Long 的valueOf会缓存 - 128~127之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象
注意:
1、Byte, Short, Long 缓存的范围都是-128-127
2、Character 缓存的范围是 0-127 (都是正数)
3、Boolean 缓存了 TRUE 和 FALSE
4、Integer的默认范围是 -128~127,最小值 - 128不能变,但最大值可以通过调整虚拟机的参数 “-5、Djava.lang.Integer.IntegerCache.high “来改变
- String 串池
- BigDecimal, BigInteger 也是有其中一部分数字使用了缓存实现了享元模式。
final关键字的原理
https://blog.csdn.net/weixin_41951205/article/details/123193025?
设置 final 变量的原理
- 理解了 volatile 原理 (读写屏障),再对比 final 的实现就比较简单了
对应字节码public class TestFinal {
final int a = 20;
}
发现被 final修饰的变量赋值也会通过putfield 指令来完成,同样在这条赋值指令之后也会加入写屏障,写屏障的作用是保证在写屏障之前的指令不会被重排序到写屏障后面,且在写屏障之前的赋值操作都会被同步到主内存中,也就是说如果没有被final修饰的变量在设置值的时候,有可能其他线程会读取到没有被赋值之前的初始值0,而有了final修饰之后就可以保证在其它线程读到它的值时不会出现为0的情况。
获取 final 变量的原理
….