并发性越来越成为现代应用程序的一个非常重要的方面。当我们扩展到更高级别的流量时,就更需要多个并发执行线程。因此,依赖注入器管理的对象的角色是极其重要的。其中,单例对象是一个特别重要的例子。
在一个大型的应用中,其qps可以高达几百,缺乏良好设计的单例对象可能成为严重的性能瓶颈。它制约着系统的性能,甚至在一定条件下,导致系统无法扩展。
应用糟糕的并发表现要比我们想象的更常见。由于并发问题通常只有在压测的时候才出现影响,这让定位和解决并发问题非常困难。因此,学习单例在并发中对程序的影响是十分必要的。在这个问题中,可变性是一个至关重要的因素。不可变的概念中也包含了一些陷阱,所以让我们来探究一下不可变到底意味着什么。
不可变量 陷阱 #1
以下类:Book
是不可变的吗?
public class Book {
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
回答 #1
答案显而易见:Book
不是不可变量。field
字段可以随意的通过setField()
方法进行修改。只需要将field
字段声明为final,便可使Book
成为不可变量,例如:
public class ImmutableBook {
private final String title;
public ImmutableBook(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
一旦在构造方法中初始化了title
字段,便不再可修改。
不可变量 陷阱 #2
以下类:AddressBook
是不可变的吗?
public class AddressBook {
private final String[] names;
public AddressBook(String[] names) {
this.names = names;
}
public String[] getNames() {
return names;
}
}
回答 #2
names
在构造方法中被赋值,同时被声明为final,所以AddressBook
应该是不变量,对吗?不对!实际上,由于names
是一个数组,只有指向该数组的引用是不可变的,而数组本身却是可变的。以下代码是完全合法的,但在多线程环境中,却可能对程序造成意外伤害:
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]);
}
}
mutate
方法摧毁性的修改了names
数组,即使指向它的引用从未发生改变。如果你运行这个程序,它会为书中的每个名字打印出“Censored!”。这个问题的唯一的解决方案是不使用数组,或者在文档中注明需谨慎地使用它们。在可能的情况下,最好使用库集合(比如java.util中的那些)类,因为这些类可以由不可修改的包装器保护。
不可变量 陷阱 #3
以下类:BetterAddressBook
是不可变的吗?
public class BetterAddressBook {
private final List<String> names;
public BetterAddressBook(List<String> names) {
this.names = Collections.unmodifiableList(names);
}
public List<String> getNames() {
return names;
}
}
回答 #3
谢天谢地,BetterAddressBook
是不可变的。Collections库提供的包装器确保一旦设置了列表,就不能对其进行更新。
不可变量 陷阱 #4
这是陷阱 #3的变体。以我们之前看到的BetterAddressBook
类为例。不允许修改BetterAddressBook
代码的前提下,我们是否有办法改变它呢?
回答 #4
答案很简单:
List<String> physicists = new ArrayList<String>();
physicists.addAll(Arrays.asList("Landau", "Weinberg", "Hawking"));
BetterAddressBook book = new BetterAddressBook(physicists);
physicists.add("Einstein");
此时,如果我们遍历BetterAddressBook
中的names
,便会发现其发生了修改:
for (String name : book.getNames())
System.out.println(name);
因此,我们必须修改回答3中的描述:BetterAddressBook
只有在其依赖的变量任何地方都不发生外泄时才是不可变的。例如,我们可以重构BestAddressBook
,在其构造方法中对传入的数组进行拷贝。
@Immutable
public class BestAddressBook {
private final List<String> names;
public BestAddressBook(List<String> names) {
this.names = Collections.unmodifiableList(new ArrayList<String>
(names));
}
public List<String> getNames() {
return names;
}
}
此时,便不存在内存泄露而修改BestAddressBook
的数组。
List<String> physicists = new ArrayList<String>();
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<Book> books;
public Library(List<Book> books) {
this.books = Collections.unmodifiableList(new ArrayList<Book>(books));
}
public List<Book> getBooks() {
return books;
}
}
回答 #5
Library
依赖一组Book
列表,但是非常小心的将入参的books
列表使用unmodifiableList进行了复制,同时,books引用也被声明为final。所有的一切看起来都很棒,难道不是吗?实际上,Library依然是可变的。虽然book数组不可被修改,但book对象依然可以修改。例如:
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注释在传递和记录这个意图方面有很大帮助。