并发与不可变性

  • 2009-08-18
  • 本文字数:3276 字

    阅读完需:约 11 分钟

对于今天的应用程序来说,并发是一个重要的、也愈发受到关注的方面。随着交易量的增加、业务日趋复杂,对大量并发线程的需求也越来越急迫。另外,由依赖注入管理的对象在应用程序中的其角色也极为关键。 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 中文站用户讨论组中与我们的编辑和其他读者朋友交流。