文章目录
前言
在多线程并发编程中,为了保证对象状态的原子性、可见性和有序性,开发者通常会使用 synchronized 等同步机制。
然而,过多的同步不仅会增加代码复杂度,还会影响性能。
如果我们将类设计为不可变的,那么对象的状态自始至终都不会改变。
这样一来,每次需要修改状态时都会产生一个全新的对象供不同线程使用,从而从根本上消除了并发安全问题。
一、什么是不可变类
一个类的对象在通过构造方法创建后,如果其状态绝对不会再被改变,那么这个类就是不可变(Immutable)类。
不可变对象具有以下核心特征:
- 所有成员变量的赋值操作仅在构造方法中完成。
- 不对外提供任何
setter方法,彻底切断外部类修改其内部状态的途径。
二、常见的不可变类
提到不可变类,Java中最经典的就是 String 类,此外还包括 Integer、Long 等基本数据类型的包装类。String 类被设计为不可变的,主要基于以下三个方面的考量:
-
常量池的需要
字符串常量池是Java堆内存中的一个特殊存储区域。创建
String对象时,如果该字符串在常量池中不存在,则新建一个;如果已存在,则直接复用已有的引用地址。这种机制能够大幅减少JVM的内存开销,提升运行效率。 -
hashCode 缓存
由于字符串状态不可变,在其创建之初
hashCode就可以被计算并缓存起来。这使得String非常适合作为哈希表(如HashMap)的键,多次调用只需返回同一个哈希值,极大地提高了检索效率。 -
线程安全
不可变对象天然具备线程安全性。在多线程环境下共享
String对象时,不会出现不可预期的结果,因此也无需进行额外的同步处理。即使我们调用String类的trim()、substring()或toLowerCase()等方法,原字符串对象依然完好无损,方法会返回一个经过处理的全新对象。

三、代码实践
理解不可变类很容易,但手写一个严谨的自定义不可变类需要严格遵守以下四个条件:
- 确保类是 final 的:防止该类被其他类继承,从而避免子类重写方法破坏其不可变性。
- 确保所有的成员变量是 final 的:强制字段只能在构造方法中初始化,后续绝无法被修改。
- 不提供任何 setter 方法:关闭外部修改对象状态的入口。
- 如果包含可变对象,必须返回副本 :这是最容易忽略的一点。如果不可变类内部持有其他可变类的对象(例如一个普通的
Book类),在对外提供该对象的getter方法时,必须返回该对象的防御性副本,绝不能直接暴露其底层的引用地址。否则,外部代码可以通过该引用地址轻易篡改原对象的数据。
代码实现示例:
Java
// 一个普通的可变类
public class Book {
private String name;
private int price;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getPrice() { return price; }
public void setPrice(int price) { this.price = price; }
}
// 自定义的不可变类
public final class Writer {
private final String name;
private final int age;
private final Book book;
public Writer(String name, int age, Book book) {
this.name = name;
this.age = age;
this.book = book;
}
public String getName() { return name; }
public int getAge() { return age; }
// 核心点:必须返回内部可变对象的克隆副本,防止引用地址外泄
public Book getBook() {
Book clone = new Book();
clone.setName(this.book.getName());
clone.setPrice(this.book.getPrice());
return clone;
}
}
在上面的测试用例中,如果不采用防御性拷贝返回 clone 对象,外部对返回的 Book 进行 setPrice 操作时,就会破坏 Writer 类的不可变性。

总结
不可变类通过牺牲极少量的内存(修改时需创建新对象),换取了多线程环境下的绝对安全。
在实际开发中,尤其是构建多线程高并发系统时,这种"无锁化"的设计思路极为重要