小米最近在帮一个朋友辅导 Java 社招面试,结果刚坐下来,朋友就皱着眉头抛出一个问题:
“什么是不可变对象?它对写并发应用有什么帮助?”
小米乐呵呵地喝了口咖啡,心想:“这个问题不难,但要讲清楚还是得花点功夫。”于是,小米决定用讲故事的方式,把这个话题拆解开来,让朋友听得轻松又明白。
不变的承诺——什么是不可变对象?在 Java 世界里,对象的状态千变万化,但也有一类对象,它们生来就被刻上了“不可变”的烙印。这类对象就是不可变对象(Immutable Objects)。
通俗来说,不可变对象就是——一旦创建,状态(即对象的数据/属性值)就不能再改变。反之,那些创建后可以修改状态的对象,则是可变对象(Mutable Objects)。
为了让朋友更好地理解,小米举了个生活中的例子:
“想象一下你在银行开了一张定期存单,你把 10 万块存进去后,这个金额就不能再改了,除非重新开一张新的存单。这张存单就是不可变对象。”
“而你的活期账户呢?存钱取钱随时变动,这就是可变对象。”
朋友点了点头,表示有点意思。
那么,如何定义一个不可变对象呢?
不可变类的“护城河”——如何实现不可变对象?在 Java 中,官方库中已经提供了不少不可变类,比如:
String
Integer(以及其他基本类型的包装类,如 Long、Double、Boolean 等)
BigInteger
BigDecimal
为什么这些类是不可变的?其实是因为它们遵循了一些严格的规则:
定义不可变对象的三个条件
对象的状态在创建后不能再修改。(即,一旦赋值就不能变)
所有的字段都必须是 final 类型。(确保赋值后不会再被修改)
对象在创建期间,不能发生 this 引用逸出。(即,不能在构造函数中把 this 传给其他线程)
为了让朋友更直观地理解,小米写了个代码示例:
为什么 ImmutablePerson 是不可变的?
name 和 age 都是 final,一旦赋值,无法更改。
没有提供 setter 方法,避免外部修改字段。
ImmutablePerson 类声明为 final,防止子类篡改不可变性。
不可变对象的“超能力”——它对并发编程有什么帮助?朋友听到这里,疑惑地问道:
“你讲了这么多,那它对并发编程到底有什么帮助?”
小米笑了笑,说:
“不可变对象对写并发应用最大的帮助,就是它能保证‘线程安全’,并且不需要额外的同步手段。”
1. 不可变对象天然线程安全
在多线程环境下,共享数据是个大问题。如果多个线程修改同一个对象,可能会导致数据不一致或线程安全问题。因此,我们通常需要加锁,比如 synchronized 或 ReentrantLock。
但是,如果使用的是不可变对象,情况就大不一样了:
因为不可变对象的状态一旦创建就不会改变,所以多个线程可以随意访问,而不必担心数据被篡改!
举个例子,我们来看下面的可变对象 MutableCounter:
如果多个线程同时调用 increment(),就可能会出现数据不一致的情况,比如两个线程同时读取 count = 5,然后各自加 1,最后 count 可能还是 6,而不是 7。
但是如果改成不可变对象,就不会有这个问题了:
这里的 increment() 方法不修改原对象,而是返回一个新的 ImmutableCounter 实例。
这样,每个线程拿到的 ImmutableCounter 都是独立的,不会发生数据竞争,自然就线程安全了。
2. 不可变对象保证了内存可见性
在 Java 并发编程中,一个经典的问题是内存可见性。如果一个对象被一个线程修改后,另一个线程可能无法立刻看到最新的变化。
但是,不可变对象的 final 字段保证了对象的初始化过程是安全的,多个线程访问时可以立即看到最新值,而不需要额外的同步手段。
Java 语言规范规定:
“在构造函数执行完毕后,final 变量的值对所有线程立即可见。”
所以,不可变对象的值不会被 CPU 缓存优化或者编译器重排序影响,从而保证了可见性问题。
使用不可变对象的最佳实践朋友听到这里,已经明白了不可变对象的价值,但又问了个很实际的问题:
“在日常开发中,什么时候应该用不可变对象?”
小米给了几个建议:
如果对象是多个线程共享的,尽量使用不可变对象。例如:String、Integer 等。
如果对象需要频繁修改,可以使用 Copy-On-Write 方式代替同步。例如:ImmutableList、AtomicReference。
如果不可变对象太大,创建新对象的开销太高,可以考虑 ThreadLocal 或 volatile 变量。
总结小米总结了一下,不可变对象的核心点:
对象创建后状态不可修改,所有字段 final,防止 this 逃逸。
天然线程安全,多个线程可以共享,无需额外同步。
保证内存可见性,防止 CPU 缓存导致的数据不一致。
适用于多线程环境,提高并发性能。
朋友听完恍然大悟:
“怪不得 String 是不可变的,这样多线程拼接字符串时才不会有问题!”
小米哈哈一笑:“对嘛,你这不是会了吗?”
END如果你觉得这篇文章有帮助,欢迎点赞、收藏、转发,关注小米,带你学技术,不走弯路!
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!