并发与不可变性

2009 年 8 月 19 日

对于今天的应用程序来说,并发是一个重要的、也愈发受到关注的方面。随着交易量的增加、业务日趋复杂,对大量并发线程的需求也越来越急迫。另外,由依赖注入管理的对象在应用程序中的其角色也极为关键。 Singleton 就是典型的这种需求。

对于一个每分钟需要处理几百个请求的大型 Web 应用来说,如果 Singleton 设计得很糟糕,它会成为严重的瓶颈,以及系统的并发性能的短板,甚至在一些特定的条件下,会导致系统失去可伸缩性。

糟糕的并发行为可能比你想象的要普遍。并且,由于它们产生的影响只有在性能测试期间才会暴露出来,这使得识别和解决这些问题变得更加困难。因此,研究 Singleton 与并发的关系就变得很重要了。

“可变性”是这个问题的一个关键要素。“不可变性”的理念背后也有很多隐晦的陷阱。所以,我们首先讨论一下所谓的“不可变”到底是指什么。为了直接切入问题,我们将以一系列谜题的方式来探讨。

不可变性谜题#1

下面的Book类是不可变的么?

复制代码
public class Book {
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}

答案#1

这个问题的答案很简单:不是。只要调用setTitle()就可以任意修改title域的值。所以这个类不是不可变的。如果将title声明为final的,就可以让Book类成为不可变的,如下所示:

复制代码
public class ImmutableBook {
private final String title;
public ImmutableBook(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}

一旦在构造函数里面设置了title的值以后,就不能再改变它了。

不可变性谜题#2

下面的类是不可变的么?

复制代码
public class AddressBook {
private final String[] names;
public AddressBook(String[] names) {
this.names = names;
}
public String[] getNames() {
return names;
}
}

答案#2

names的值是final类型,只会在构造函数中设置一次。所以AddressBook应该是不可变的,对不对?错!事实上,容易混淆的地方在于names是一个数组,将它声明为final只会使它的引用成为不可变的。 下面的代码完全是合法的,但它却潜在地破坏了数据。而这正是多线程程序所担心的问题:

复制代码
public class AddressBookMutator {
private final AddressBook book;
@Inject
public AddressBookMutator(AddressBook book) {
this.book = book;
}
public void mutate() {
String[] names = book.getNames();
for (int i = 0; i < names.length; i++)
names[i] = "Censored!"
for (int i = 0; i < names.length; i++)
System.out.println(book.getNames()[i]);
}
}

虽然names域是不可改变的,但是方法mutate()会破坏性地改写数组。如果运行了这段程序,AddressBook中的每个名字都变成了“Censored(已篡改)”。解决这个问题的真正方法是避免使用数组,或者在真正理解它们以后非常谨慎地使用。更好的办法是使用容器类库(比如java.util),这样能够用一个不可修改的封装类来保护数组的内容。参看谜题 3,它示范了用java.util.List取代数组。

不可变性谜题#3

下面的BetterAddressBook类是不可变的么?

复制代码
public class BetterAddressBook {
private final List<string> names;<p> public BetterAddressBook(List<string> names) {<br></br> this.names = Collections.unmodifiableList(names);<br></br> }<br></br> public List<string> getNames() {<br></br> return names;<br></br> }<br></br>}</string></string></p></string>

答案#3

谢天谢地,没错,BetterAddressBook是不可变的。Collections类库中的封装类可以确保一旦设置了names的值,就不能对它再有任何更新。下面的代码虽然可以编译,却会在运行时导致异常:

复制代码
BetterAddressBook book = new BetterAddressBook(Arrays.asList("Landau", "Weinberg", "Hawking"));
book.getNames().add(0, "Montana");

不可变性谜题#4

下面是谜题 3 的变体,仍然使用我们前面的见到的BetterAddressBook类。是否存在某种构造方法,使得我仍然可以在构造以后修改它?前提是不允许修改BetterAddressBook的代码。

答案非常简单,只是有点儿混乱:

复制代码
List<string> physicists = new ArrayList<string>();<br></br>physicists.addAll(Arrays.asList("Landau", "Weinberg", "Hawking"));<br></br>BetterAddressBook book = new BetterAddressBook(physicists);<br></br>physicists.add("Einstein");</string></string>

现在遍历BetterAddressBooknames列表:

复制代码
for (String name : book.getNames())
System.out.println(name);

恩,看来,我们必须要重新审视谜题 3 中的答案了。只有满足了names列表没有泄露到BetterAddressBook类以外的前提条件,BetterAddressBook才是不可变。更好的方法是,我们能够重写一个完全安全的版本:在构造的时候复制一份列表:

复制代码
@Immutable
public class BestAddressBook {
private final List<string> names;<br></br> public BestAddressBook(List<string> names) {<br></br> this.names = Collections.unmodifiableList(new ArrayList<string><br></br>(names));<br></br> }<br></br> public List<string> getNames() {<br></br> return names;<br></br> }<br></br>}</string></string></string></string>

现在,你可以随意泄露甚至修改原来的列表了:

复制代码
List<string> physicists = new ArrayList<string>();<br></br>physicists.addAll(Arrays.asList("Landau", "Weinberg", "Hawking"));<p>BetterAddressBook book = new BetterAddressBook(physicists);</p><p>physicists.clear();</p><br></br>physicists.add("Darwin");<br></br>physicists.add("Wallace");<br></br>physicists.add("Dawkins");<p>for (String name : book.getNames())</p><br></br> System.out.println(name);</string></string>

…同时BestAddressBook不会受到任何影响:

复制代码
Landau
Weinberg
Hawking

尽管你不必每次都使用这么小心的方法,但是如果你无法确保参数是否可能泄露到其他的对象中去,那么建议你使用这种方法。

不可变性谜题#5

下面的Library类是不可变的么?(调用了谜题 1 中的 Book)

复制代码
public class Library {
private final List<book> books;<p> public Library(List<book> books) {<br></br> this.books = Collections.unmodifiableList(new ArrayList<book>(books));<br></br> }<br></br> public List<book> getBooks() {<br></br> return books;<br></br> }<br></br>}</book></book></book></p></book>

答案#5

Library依赖于一个 Book 列表,不过它非常小心地先复制一份列表,然后把它封装在一个不可变的包装类中。当然它唯一的域也是 final 的。每件事看起来都无懈可击了?事实上,Library其实是可变的!尽管 Book 的容器是不变的,但是 Book 对象自身却不是。回忆一下谜题 1 中的场景,Book 的 title 可以被修改:

复制代码
Book book = new Book();
book.setTitle("Dependency Injection")
Library library = new Library(Arrays.asList(book));
library.getBooks().get(0).setTitle("The Tempest"); //mutates Library

不可变性和对象图的黄金规则是每一个被依赖的对象也必须是不可变的。在BestAddressBook中,我们很幸运,因为 Java 中的 String 已经是不可变的了。在声明一个“不可变”的对象以前,仔细地检查它依赖的每一个对象也都是安全不可变的。在谜题 4 中见到的@Immutable标注可以帮你传达这一意图,并将它记录到文档中。


本文是 Manning Publications 即将出版的新书《 Dependency Injection 》的节选,该书作者是 Dhanji R. Prasanna。文中通过五个谜题大致浏览了不可变性的概念。关于本书的目录、作者论坛以及其他资源,请访问 http://manning.com/prasanna/

查看英文原文 Concurrency and Immutability

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2009 年 8 月 19 日 00:304064
用户头像

发布了 53 篇内容, 共 89040 次阅读, 收获喜欢 2 次。

关注

评论

发布
暂无评论
发现更多内容

CRM企业到底该不该做PaaS?

ToB行业头条

PaaS SaaS CRM

云原生技术采用增加,全球60%后端开发人员都在使用容器 | 趋势分享

博云技术社区

云计算 容器 云原生 PaaS 博云

usdt承兑跑分系统开发,区块链支付跑分系统搭建

WX13823153201

usdt承兑跑分系统开发

华为云会议的前世今生

华为云开发者社区

直播 云服务 华为云 视频编码 视频会议

有选择才会有困惑

escray

学习 面试 面试现场

Docker 镜像构建之 Dockerfile

哈喽沃德先生

Docker 容器 微服务 容器技术 容器化

从6大应用场景,看边缘计算落地生根

博云技术社区

边缘计算 PaaS 容器云 云平台 博云

面试官再问你Http请求过程,怼回去!

架构师修行之路

HTTP TCP/IP

温故知新——Spring AOP(二)

牛初九

spring aop ioc

“全球+”浪潮下,企业出海选择合适的“技术船舶”成关键

华为云开发者社区

网络 华为云 企业出海 网络加速 宽带

莱卡、宾利都在用,英特尔oneAPI渲染工具带来高质量视觉体验

intel001

我也没想到 Springboot + Flowable 开发工作流会这么简单

程序员内点事

java 14

GrowingIO AWS 成本优化之路

GrowingIO技术专栏

AWS 成本优化

【Elasticsearch 技术分享】—— ES 查询检索数据的过程,是什么样子的?

程序员小航

Java elasticsearch 搜索 ES Lucene Elastic Search

Java | 你知道快速搭建一个spring boot项目该怎么做吗?

简爱W

面经手册 · 第7篇《ArrayList也这么多知识?一个指定位置插入就把谢飞机面晕了!》

小傅哥

Java 数据结构 小傅哥 面试题 ArrayList

新金融分布式架构之SOFAStack解决方案

阿里云金融线TAM SRE专家服务团队

硬核科技:莱克立式吸尘器,引领家居清洁“新态度”

InfoQ_967a83c6d0d7

使用 K8s 进行作业调度实战分享

后端进阶

学习 Kubernetes 容器 k8s 调度式分布

性能相关,内存

Linuxer

性能

炒股不要看K线图(分享最近学习投资的一点心得)

Nick

投资 理财

Redis系列(一):Redis简介及环境安装

简爱W

Flink-键值分区状态-10

小知识点

scala 大数据 flink

MySQL redo与undo日志解析

Simon

MySQL Redo MySQL日志

币期权DAPP 8月28日全球同步耀世上线,掀起币圈追捧热潮

InfoQ_967a83c6d0d7

Android |《看完不忘系列》之dagger

哈利迪

android

Luajit字节码分析之KSTR

whosemario

lua

零代码简史

明道云

SaaS

第11周总结+作业

林毋梦

Redis系列(二):Redis的5种数据结构及其常用命令

简爱W

难以遏制的人因差错-Go的日志工具之痛

田晓亮

go 微服务

并发与不可变性-InfoQ