介绍不变模式的文章有很多,这里就不详细介绍什么是不变模式以及它的实现方式了。本文主要详细阐述一下不变模式的线程安全的个人理解。
一、为什么说不变模式是线程安全的?
使用不变模式实现的类是线程安全的,因为不变模式实现的类不会对外开放修改内部状态和数据的方法。不可变的对象一旦创建,其内部状态和数据不再发生任何变化。
也就是说多线程访问一个不可变对象时,只能读取,不能修改。既然只能读取,不能修改,也就不会在多线程操作场景下出现数据不一致。所以说不变模式通过回避问题而不是解决问题的态度来处理多线程并发访问控制。
二、JDK里不变模式实现的类是线程安全的吗?
在JDK里,所有包装类都是使用不变模式实现的,比如java.lang.Integer。按照上面说的,不变模式实现的类应该都是线程安全的,那么Integer类应该也是线程安全的。但是实时如此吗?
对Java多线程操作稍微有点了解的同学应该都会说Integer类型不是线程安全的,如果不了解的同学可以看一下下面的这个demo:
public class Incrementer {
private Integer count = 0;
public void increment() {
count += 1; // 自动拆箱和自动装箱
}
public Integer getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
final Incrementer incrementer = new Incrementer();
// 创建并启动多个线程来递增计数器
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
incrementer.increment();
}
});
threads[i].start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
// 输出最终的计数结果
System.out.println("Final count is: " + incrementer.getCount());
}
}
如果Integer是线程安全的,最终的计算结果应该是10000,但是你会发现每次运行的结果都比10000小很多。这个结果显然是和前面说的“不变模式是线程安全的”结论相违背了。
实际上,Integer对象本身确实是不可变的,而对一个Integer变量进行赋值时,会创建一个新的Integer对象,将这个Integer对象赋给原来的Integer对象。以上面的count+=1为例,这个操作包含了拆箱、加法运算、装箱(创建新的Integer对象)、赋值四个操作。这四个操作显然不是原子的,多线程操作时,必然会导致数据不一致。
所以严格意义上来说,Integer类型并不是线程安全的。
三、总结
总结一下:
- 不变模式确实是线程安全的,因为无法修改,所以不存在多线程下操作的数据不一致性。
- Integer等封装类型在读写混合的场景,实际上是没法保证线程安全的,因为变量本身不是不可变的,修改值的时候,会创建一个新的对象进行赋值,该过程不满足原子性。