InfoQ

InfoQ

文章

我的书签

登录注册 以永久保存书签。

该内容已经被标记书签!

标记书签错误,请重试!

并发与不可变性

作者 Dhanji Prasanna 译者 韩锴 发布于 2009年8月19日

领域
架构 & 设计,
语言 & 开发
主题
设计 ,
编程 ,
Java ,
面向对象设计
标签
并发

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

老外就是有意思,扣的很仔细 发表人 wangzm zhimeng 发表于
忽略这种不变性会带来什么麻烦? 发表人 He Yiding 发表于
看来不可变性是很难控制的啊 发表人 he chris 发表于
Re: 看来不可变性是很难控制的啊 发表人 Lee Albert 发表于
  1. 返回顶部

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

    发表人 wangzm zhimeng

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

  2. 返回顶部

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

    发表人 He Yiding

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

  3. 返回顶部

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

    发表人 he chris

    并发是原罪啊

  4. 返回顶部

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

    发表人 Lee Albert

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

深度内容

大规模视频网站的计费与流量管理

本次分享将会就大规模视频网站的计费与流量管理这个话题,从操作层面细细进行讲解和分析,为系统工程师们揭示平日里我们没有关心的另一些内容。同时也希望本次分享能揭示行业中的一些“潜规则”,让互联网行业的流量与带宽管理更为开放与简洁。
本次演讲视频录制于QCon杭州2011

专访Jeffery Richter:Windows 8是微软的重中之重

Jeffery Richter以其多本Windows核心技术的经典著作而闻名,同时,他深入掌握微软的.NET等一系列核心技术,2012年1月,Jeffery Richter在北京接受了InfoQ中文站的专访,谈到Windows 8和WinRT编程,并就异步编程、Windows编程中的可扩展性、性能和安全性方面给出自己的建议。

应用云平台的可用性——从新浪SAE看云平台设计

云计算平台的可用性,相比传统互联网服务而言,更加复杂和困难,也更具有挑战性。本文借助新浪SAE云平台为读者讲述了云平台可用性的定义、如何打造高可用的平台,以及对云计算的用户提出了建议。

JVM定制改进 @ 淘宝

淘宝高度重视Java平台的健康发展,组建了一个团队专注于Java平台的底层部分的性能、功能与稳定性改进;工作主要基于OpenJDK中的HotSpot VM开展,其中一些通用的功能随后也会逐渐反馈给OpenJDK社区。希望能与使用Java平台开发应用的大家交流经验。
本次演讲视频录制于QCon杭州2011

"伤得起"的云计算应用——对云端应用之架构的思考

2011年4月21日至22日是值得云计算从业者纪念的日子。Amazon的IaaS服务出现故障,导致许多商业网站的服务中断,影响非常严重。作为云计算用户,我们需要思考的是,如何保证即便在云服务不可用的情况,我们的应用架构仍然能够屹立不倒?本文正是站在云计算用户的角度试图探讨这一问题。

让交付的速度跟上思考的速度

12人的技术团队,4组刀片服务器,每月20亿的访问量,每日1次准时部署,99.9%的可用性。这可能吗?当然。想知道如何做的吗?百姓网将与您分享他们在DevOps实践过程中的经验和技巧。
本次演讲视频录制于QCon杭州2011

架构之路——穿行在产品和业务之间

篱笆作为一家起源于社区的电子商务公司,反映到技术层面就是同时要面对产品和业务,以及经营战略的变化调整。如何在产品和业务的夹缝之间完成技术架构的抽象与平衡,寻找更有效的价值定位,这当中有些经验教训和个人感悟愿与众人分享。
本次演讲视频录制于QCon杭州2011

特性注入:成功三部曲

本文将对特性注入以及相关方法做一个扫盲性的介绍。我们会解释这个框架的关键要素,并附上实例来证实它们。为了让文章保持相对较短,我们不会深入到某个工具或方法中,而是会给出一些参考资料,以便大家做进一步的研究。