阿里P7面试官常问:不可变对象如何优化并发?

软件求生 2025-03-08 10:20:19



小米最近在帮一个朋友辅导 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岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!

0 阅读:0
软件求生

软件求生

从事软件开发,分享“技术”、“运营”、“产品”等。