BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

并发与不可变性

| 作者 Dhanji Prasanna 关注 0 他的粉丝 ,译者 韩锴 关注 0 他的粉丝 发布于 2009年8月20日. 估计阅读时间: 13 分钟 | ArchSummit北京2018 共同探讨机器学习、信息安全、微服务治理的关键点

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

public BetterAddressBook(List names) {
this.names = Collections.unmodifiableList(names);
}
public List getNames() {
return names;
}
}

答案#3

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

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

不可变性谜题#4

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

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

List physicists = new ArrayList();
physicists.addAll(Arrays.asList("Landau", "Weinberg", "Hawking"));
BetterAddressBook book = new BetterAddressBook(physicists);
physicists.add("Einstein");

现在遍历BetterAddressBooknames列表:

for (String name : book.getNames())
System.out.println(name);

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

@Immutable
public class BestAddressBook {
	private final List names;
public BestAddressBook(List names) {
this.names = Collections.unmodifiableList(new ArrayList
(names));
}
public List getNames() {
return names;
}
}

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

List physicists = new ArrayList();
physicists.addAll(Arrays.asList("Landau", "Weinberg", "Hawking"));


BetterAddressBook book = new BetterAddressBook(physicists);


physicists.clear();
physicists.add("Darwin");
physicists.add("Wallace");
physicists.add("Dawkins");

for (String name : book.getNames())
System.out.println(name);

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

Landau
Weinberg
Hawking

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

不可变性谜题#5

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

public class Library {
	private final List books;

public Library(List books) {
this.books = Collections.unmodifiableList(new ArrayList(books));
}
public List getBooks() {
return books;
}
}

答案#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中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

老外就是有意思,扣的很仔细 by wangzm zhimeng

老外的精神值得学习,呵呵,扣的真仔细。
不过final只要记住是修饰的当前对象引用不可变就可以了。
如果真的理解了这句话,就不存在上边这么绕的问题了。
如果理解的不准确,随便绕都能把人绕晕的。呵呵。

忽略这种不变性会带来什么麻烦? by He Yiding

看起来似乎是增加了不必要的逻辑。

看来不可变性是很难控制的啊 by he chris

并发是原罪啊

Re: 看来不可变性是很难控制的啊 by Lee Albert

反过来说可变状态才是原罪。而并发是原始需求。

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

4 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT