并发与不可变量

并发性越来越成为现代应用程序的一个非常重要的方面。当我们扩展到更高级别的流量时,就更需要多个并发执行线程。因此,依赖注入器管理的对象的角色是极其重要的。其中,单例对象是一个特别重要的例子。

在一个大型的应用中,其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注释在传递和记录这个意图方面有很大帮助。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容