不可变类与线程安全
大家好,我是欧阳方超,微信公众号同名。
1 概述
不可变对象是不可变类的一个实例。它是一个具体的对象,其状态在创建之后不能被改变。比如String就是不可变类(immutable class),一旦初始化后,相应的字符串对象就成为不可变对象(immutable object)。不可变对象的状态是不能改变,当试图修改其状态时,会得到一个新的不可变对象。所以不可变类有一个突出的优势——天然线程安全,在多线程环境中不用采取额外措施来保证线程安全。
2 不可变类
2.1 创建不可变类的原则
先说一些概念性的内容,创建不可变类一般要遵守下面的原则:
- 使用final将类声明为不可继承,防止其他类继承并修改其行为;
- 使用private将所有成员变了设置为私有的,防止外部直接访问;
- 不为成员变量提供setter方法;
- 所有可变对象都设置为final类型的,确保它们只被赋值一次;
- 使用构造方法初始化所有成员变量,并且执行深拷贝;
- 在getter方法中使用构造函数进行深拷贝,返回对象的副本,而不是返回实际对象的引用,以防止外部修改。
2.2 验证不可变类的不可变性
public class ImmutableTest {
public static void main(String[] args) {
//
int id = 1;
String name = "immutable";
HashMap<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
FinalClassDemo finalClassDemo = new FinalClassDemo(id, name, map);
System.out.println("object state first assigned:" + JSON.toJSONString(finalClassDemo));
HashMap<String, String> map1 = finalClassDemo.getMap();
map1.put("key3", "value3");
System.out.println("try to change object state:" + JSON.toJSONString(finalClassDemo));
}
}
final class FinalClassDemo {
private final int id;
private final String name;
private final HashMap<String, String> map;
public FinalClassDemo(int id, String name, HashMap<String, String> map) {
this.id = id;
this.name = name;
HashMap hashMap = new HashMap(map);
this.map = hashMap;
}
public HashMap<String, String> getMap() {
return new HashMap<String, String>(map);
}
}
上面的示例中,FinalClassDemo是一个不可变类,在主类ImmutableTest中创建了一个不可变对象,之后试图修改这个不可变对象中的可变成员map,发现并没有修改成功:
object state first assigned:{"map":{"key1":"value1","key2":"value2"}}
try to change object state:{"map":{"key1":"value1","key2":"value2"}}
如果不使用深拷贝,即在构造方法直接把引用赋给待创建的对象的map,get方法中直接返回map的引用,可以看下将会出现什么结果:
public class ImmutableTest {
public static void main(String[] args) {
//
int id = 1;
String name = "immutable";
HashMap<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
FinalClassDemo finalClassDemo = new FinalClassDemo(id, name, map);
System.out.println("object state first assigned:" + JSON.toJSONString(finalClassDemo));
HashMap<String, String> map1 = finalClassDemo.getMap();
map1.put("key3", "value3");
System.out.println("try to change object state:" + JSON.toJSONString(finalClassDemo));
}
}
final class FinalClassDemo {
private final int id;
private final String name;
private final HashMap<String, String> map;
public FinalClassDemo(int id, String name, HashMap<String, String> map) {
//
this.id = id;
this.name = name;
//HashMap hashMap = new HashMap(map);
this.map = map;
}
public HashMap<String, String> getMap() {
return map;
}
}
执行上面程序,输出如下:
object state first assigned:{"map":{"key1":"value1","key2":"value2"}}
try to change object state:{"map":{"key1":"value1","key2":"value2","key3":"value3"}}
可以看出程序的状态被改变了,因此不可变类中如果有可变成员变量,在构造方法中一定要用深拷贝方式为其赋值,其get方法中也要使用深拷贝方式返回该成员变量的副本。
3 不可变类与线程安全
如果不可变类中,可变对象在初始化及get方法中执行的是深拷贝,那么该不可变类是线程安全的,如果执行的是浅拷贝,则是非线程安全的,下面通过示例进行说明。
3.1 深拷贝示例:线程安全
深拷贝确保每个对象都被完全复制,包括其内的可变对象。这意味着即使外部代码修改了可变对象,原始对象的状态也不会受到影响。
public class ImmutableTest {
public static void main(String[] args) {
//
int id = 1;
String name = "immutable";
HashMap<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
FinalClassDemo finalClassDemo = new FinalClassDemo(id, name, map);
//启动多个线程修改返回的数据
Runnable runnable = () -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
finalClassDemo.getMap().put("key3", "value3"); //尝试修改返回的数据
};
Thread thread = new Thread(runnable);
thread.start();
Thread thread1 = new Thread(runnable);
thread1.start();
try {
thread.join();
thread1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//finalClassDemo对象被两个线程修改了map,但是不影响finalClassDemo对象的map
System.out.println(JSON.toJSONString(finalClassDemo));
}
}
final class FinalClassDemo {
private final int id;
private final String name;
private final HashMap<String, String> map;
public FinalClassDemo(int id, String name, HashMap<String, String> map) {
//
this.id = id;
this.name = name;
HashMap hashMap = new HashMap(map);
this.map = hashMap;
}
public HashMap<String, String> getMap() {
return new HashMap<String, String>(map);
}
}
上面的示例中,即使两个线程尝试修改getMap()返回的HashMap,原始finalClassDemo对象的数据依然保持不变,如下,显示了其线程安全性。
{"map":{"key1":"value1","key2":"value2"}}
3.2 浅拷贝示例:非线程安全
浅拷贝只复制对象的引用,而不复制引用所指向的对象。这意味着如果多个对象共享同一个可变对象,修改其中一个对象会影响到其他对象。
public class ImmutableTest {
public static void main(String[] args) {
//
int id = 1;
String name = "immutable";
HashMap<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
FinalClassDemo finalClassDemo = new FinalClassDemo(id, name, map);
Runnable runnable = () -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
finalClassDemo.getMap().put("key3", "value3");
};
Thread thread = new Thread(runnable);
thread.start();
Thread thread1 = new Thread(runnable);
thread1.start();
try {
thread.join();
thread1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//finalClassDemo对象被两个线程修改了map,原始的finalClassDemo对象也被改变了
System.out.println(JSON.toJSONString(finalClassDemo));
}
}
final class FinalClassDemo {
private final int id;
private final String name;
private final HashMap<String, String> map;
public FinalClassDemo(int id, String name, HashMap<String, String> map) {
//
this.id = id;
this.name = name;
this.map = map;
}
public HashMap<String, String> getMap() {
return map;
}
}
上面的示例中getMap()方法返回的是对同一个对象map的引用,因此多个线程对该数据的修改会相互影响,导致数据的不一致性,显示了其非线程安全性。
{"map":{"key1":"value1","key2":"value2","key3":"value3"}}
4 不可变类使用场景
你有没有感觉奇怪,既然不可变类在初始化后,其状态就不能改变了,那要这样的类有什么用呢,因为我一直感觉所谓程序的执行无非是若干对象的状态在发生变化而已。不过不可变类确实有它的使用场景,比如作为数据传输对象(DTO),确保在不同层之间传输的数据不被意外修改。另外不可变类天然是线程安全的,因为其状态在创建后无法改变。在多线程环境中使用不可变对象可以减少同步需求,从而提升性能和简化代码结构。
4.1 不可变类的优势
- 线程安全
原理:不可变类的状态在创建后不能被修改,所以多个线程访问不可变对象时不会出现数据不一致的情况。因为不存在一个线程修改对象状态而导致其他线程读取到错误状态的风险。 - 易于理解和调试
原理:不可变类的行为是可预测的。由于对象状态不能改变,开发人员在阅读代码和调试时,不需要考虑对象状态在不同地方被意外修改的情况。这使得代码逻辑更加清晰,更容易追踪和理解程序的执行流程。 - 可缓存性好
原理:由于不可变对象的状态固定,所以可以安全地在缓存中存储和重复使用。一旦计算或获取了不可变对象的值,就可以将其存储在缓存中,下次需要相同的对象时,直接从缓存中获取,避免了重复计算或获取的开销。
4.2 不可变类的劣势
- 占用更多内存(可能)
原理:对于包含大量可变数据的不可变类,如果需要频繁修改数据,可能会导致创建大量新的不可变对象,从而占用更多的内存空间。因为每次修改实际上是创建一个新的对象,而旧对象如果没有被及时回收,就会占用额外的内存。
5 总结
介绍创建不可变类需要遵守的原则,通过详细示例介绍了在使用深度拷贝的前提下,不可变类天然具备线程安全性,最后介绍了不可变类的使用场景并介绍其优劣势。
我是欧阳方超,把事情做好了自然就有兴趣了,如果你喜欢我的文章,欢迎点赞、转发、评论加关注。我们下次见。