Fork me on GitHub

设计模式

设计原则

1.针对接口编程,而不是针对实现编程

2.”针对接口编程“真正的意思是“针对超类型(supertype)编程”

3.多用组合少用继承

4.为了交互对象之间的松耦合设计而努力

5.类应该对扩展开放,对修改关闭

6.要依赖抽象,不要依赖具体类(依赖倒置原则)

7.最少知识原则:只和你的密友谈话

8.别调用我们,我们会调用你

9.一个类应该只有一个引起变化的原因

松耦合的威力

当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节,观察者模式提供了一种对象设计,让主题和观察者之间松耦合

松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降低了最低

策略模式

定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户

要点

1.策略模式通常会用行为或算法配置Context类

观察者模式

定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者会收到通知并自动更新

要点

1.观察者模式定义了对象之间一对多的关系

2.主题(也就是可观察者)用一个共同的接口来更新观察者

3.观察者和可观察者之间用松耦合方式结合(loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口

4.使用此模式时,你可从被观察者处推(push)或拉(pull)数据(然而,推的方式被认为更“正确”)。

5.有多个观察者时,不可以依赖特定的通知次序

6.Java有多种观察者模式的实现,包括了通用的java.util.Observable实现上所带来的一些问题

7.如果有必要的话,可以实现自己的Observable,这并不难,不要害怕

8.Swing大量使用观察者模式,许多GUI框架也是如此

9.此模式也被应用在许多地方,例如:JavaBeans,RMI

装饰者模式

装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案

装饰者和被装饰对象有相同的超类型

你可以用一个或多个装饰者包装一个对象

既然装饰者和被装饰者有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象替代它

装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的

对象可以在任何时候被装饰,所以可以在运行时动态地,不限量地用你喜欢的装饰者来装饰对象

要点

1.继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式

2.在我们的设计中,应该允许行为可以被扩展,而无须修改现有的代码

3.组合和委托可用与在运行时动态地加上新的行为

4.除了继承装饰者模式也可以让我们扩展行为

5.装饰者模式意味着一群装饰者类,这些类用来包装具体组件

6.装饰者类放映处被装饰的组件类型(事实上,它们具有相同的类型,都经过接口或继承实现)

7.装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的

8.你可以用无数个装饰者包装一个组件

9.装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型

10.装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂

简单工厂方法模式

定义了一个创建对象的接口,但由于子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类

依赖倒置原则

避免违反

1.变量不可以持有具体类的引用 (如果使用new,就会持有具体类的引用。你可以改用工厂避开这样的做法)

2.不要让类派生自具体类 (如果派生自具体类,你就会依赖具体类,请派生自一个抽象[接口或抽象类])

3.不要覆盖基类中已实现的方法 (如果覆盖基类已实现的方法,那么你的基类就不是一个真正适合被继承的抽象。基类中已实现的方法,应该由所有的子类共享)

要点

1.简单工厂,虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体类解耦

2.工厂方法使用继承,把对象的创建委托给子类,子类实现工厂方法来创建对象

3.工厂方法允许类将实例化延迟到子类进行

4.工厂是很有威力的技巧,帮助外面针对抽象编程,而不要针对具体类编程

抽象工厂模式

提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类

要点

1.所有的工厂都是用来封装对象的创建

2.抽象工厂使用对象组合,对象的创建被实现在工厂接口所暴露出来的方法中

3.所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合

4.抽象工厂创建相关的对象家族,而不需要依赖它们的具体类

5.依赖倒置原则,指导外面避免依赖具体类型,而要尽量依赖抽象

单例模式

确保一个类只有一个实例,并提供一个全局访问的

要点

1.单例模式确保程序中一个类最多只有一个实例

2.单例模式也提供访问这个实例的全局点

3.在Java中实现单例模式需要私有的构造器,一个静态方法和一个静态变量

4.确定在性能和资源上的限制,然后小心地选择适当的方案来实现单例,以解决多线程的问题(我们必须认定所有的程序都是多线程的)

5.小心,你如果使用多个类加载器,可能导致单例失效而产生多个实例

命令模式

将“请求”封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象,命令模式也支持可撤销的操作

要点

1.命令模式将发出请求的对象和执行请求的对象解耦

2.在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作

3.通用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用

4.调用者可以接受命令当做参数,甚至在运行时动态地进行

5.命令可以支持撤销,做法是实现一个undo()方法来回到execute()被执行前的状态

6.宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤销

7.实际操作时,很常见使用“聪明”命令对象,也就是之间实现了请求,而不是将工作委托给接收者

8.命令也可以用来实现日志和事务系统

适配器模式

将一个类的接口,转换成客户期望的另一个接口。适配器让原来接口不兼容的类可以合作无间

外观模式

提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用

要点

1.当需要使用一个现有的类而其接口并不符合你的需要时,就使用适配器

2.当需要简化并统一一个很大的接口或者一群复杂的接口时,使用外观

3.适配器改变接口以符合客户的期望

4.外观将客户从一个复杂的子系统中解耦

5.实现一个适配器可能需要一番功夫,也可能不费功夫,视目标接口的大小与复杂度而定

6.实现一个外观,需要将子系统组合进外观中,然后将工作委托给子系统执行

7.适配器模式有两种形式:对象适配器和类适配器。类适配器需要用到多重继承

8.你可以为一个子系统实现一个以上的外观

9.适配器将一个对象包装起来以改变其接口,装饰者将一个对象包装起来以增加新的行为和责任;而外观将一群对象“包装”起来以简化其接口

模板方法模式

在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤

要点

1.”模板方法”定义了算法的步骤,把这些步骤的实现延迟到子类

2.模板方法模式为我们提供了一种代码复用的重要技巧

3.模板方法的抽象类可以定义具体方法,抽象方法和钩子

4.抽象方法由子类实现

5.钩子是一种方法,它在抽象类中不做事,或者只做默认的事,子类可以选择要不要去覆盖它

6.为了防止子类改变模板方法中的算法,可以将模板方法声明为final

7.好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用底层模块

8.你将在真实世界代码中看到模板方法模式的许多变体,不要期待它们全都是一眼就可以被你认出的

9.策略模式和模板方法模式都封装算法,一个用组合,一个用继承

10.工厂方法是模板方法的一种特殊版本

迭代器模式

提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示

迭代器模式让我们能游走于聚合内的每一个元素,而不暴露其内部的表示

把游走的任务放在迭代器上,而不是聚合上,这样简化了聚合的接口和实现,也让责任各得其所

要点

1.迭代器允许访问聚合的元素,而不需要暴露它的内部结构

2.迭代器将遍历聚合的工作封装进一个对象中

3.当使用迭代器的时候,我们依赖聚合提供遍历

4.迭代器提供一个通用的接口,让我们遍历聚合的项,当我们编码使用聚合的项时,就可以使用多态

5.我们应该努力让一个类只分配一个责任

单一原则

类的每个责任都有改变的潜在区域。超过一个责任,意味者超过一个改变的区域

这个原则告诉我们,尽量让每个类保持单一责任

组合模式

允许你将对象组合成树型结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合

组合模式让我们能用树形创建对象的结构,树里面包含了组合以及个别的对象**

使用组合结构,我们能把相同的操作应用在组合和个别对象上,换句话说,在大多数情况下,我们可以忽略组合和个别对象之间的差别

要点

1.组合模式提供一个结构,可同时包容个别对象和组合对象

2.组合模式允许客户对个别对象以及组合对象一视同仁

3.组合结构内的任意对象称为组件,组件可以是组合,也可以是叶节点

4.在实现组合模式时,有许多设计上的折衷。你要根据需要平衡透明性和安全性

状态模式

允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类

要点

1.状态模式允许一个对象基于内部状态而拥有不同得行为

2.和程序状态机(PSM)不同,状态模式用类代表状态

3.Context会将行为委托给当前状态对象

4.通过将每个状态封装进一个类,我们把以后需要做得任何改变局部化了

5.状态模式和策略模式有相同的类图,但是它们的意图不同

6..状态模式允许Context随着状态的改变而改变行为

7.状态转换可以由State类或Context类控制

8.使用状态模式通常会导致设计中类的数目大量增加

9.状态类可以被多个Context实例共享

代理模式

为另一个对象提供一个替身或占位符以控制对这个对象的访问

使用代理模式创建代表(representative)对象,让代表对象控制某对象的访问被代理的对象可以是远程的对象,创建开销打的对象或需要安全的控制对象

要点

1.代理模式为另一个对象提供代表,以便控制客户对对象的访问,管理访问的方式有许多种

2.远程代理管理客户和远程对象之间的交互

3.虚拟代理控制访问实例化开销大的对象

4.保护代理基于调用者控制对象方法的访问

5.代理模式有许多变体,例如:缓存代理,同步代理,防火墙代理和写入时复制代理

6.代理在结构上类似装饰者,但是目的不同

7.装饰者模式为对象加上行为,而代理则是控制访问

8.Java内置的代理支持,可以根据需要建立动态代理,并将所有调用分配到所选的处理器

9.就和其他的包装者(wrapper)一样,代理会造成你的设计中类的数目增加

复合模式

要点

1.MVC是复合模式,结合了观察者模式,策略模式和组合模式

2.模型使用观察者模式,以便观察者更新,同时保持两者之间解耦

3.控制器是视图的策略,视图可以使用不同的控制器实现,得到不同的行为

4.视图使用组合模式实现用户界面,用户界面通常组合了嵌套的组件,像面板,框架和按钮

5.这些模式携手合作,把MVC模型的三层解耦,这样可以保持设计干净又有弹性

6.适配器模式用来将新的模型适配成已有的视图和控制器

7.Model2是MVC在Web上的应用

8.在Model2中,控制器实现成Serblet,而JSP/HTML实现视图

反模式

告诉你如何采用一个不好的解决方案解决一个问题

桥接模式

使用桥接模式(Bridge Pattern)不只改变你的实现,也改变你的抽象

桥接模式通过将实现和抽象放在两个不同的类层次中而使它们可以独立改变

适合使用在需要跨越多个平台的图形和窗口系统上

当需要用不同的方式改变接口和实现时,你会发现桥接模式很好用

优点

1.将实现予以解耦,让它和界面之间不再永久绑定

2.抽象和实现可以独立扩展,不会影响到对方

3.对于“具体的抽象类”所做的改变,不会影响到客户

缺点

桥接模式的缺点是增加了复杂度

生成器模式

使用生成器模式(Builder Pattern)封装一个产品的构造过程,并允许按步骤构造

经常被用来创建组合结构

优点

1.将一个复杂对象的创建过程封装起来

2.允许对象通过多个步骤来创建,并且可以改变过程(这和只有一个步骤的工厂模式不同)

3.向客户隐藏产品内部的表现

4.产品的实现可以被替换,因为客户只看到一个对象的接口

缺点

与工厂模式相比,采用生成器模式创建对象的客户,需要具备更多的领域知识

责任链模式

当你想要让一个以上的对象有机会能够处理某个请求对象的时候,就使用责任链模式(Chain of Responsibility Pattern)

经常被使用在窗口系统中,处理鼠标和键盘之类的事件

优点

将请求的发送者和接收者解耦

可以简化你的对象,因为它不需要知道链的结构

通过改变链内的成员或调动它们的次序,允许你动态地新增或者删除责任

缺点

并不保证请求一定会被执行,如果没有任何对象处理它的话,它可能会落到链尾端之外(这可以是优点也可以是缺点

可能不容易观察运行时的特征,有碍于出错

蝇量模式

如果让某个类的一个实例能够用来提供许多“虚拟实例”,就使用蝇量模式(Flyweight Pattern)

当一个类有许多的实例,而这些实例能被同一方法控制的时候,我们就可以使用蝇量模式

优点

减少运行时对象实例的个数,节省内存

将许多“虚拟”对象的状态集中管理

缺点

一旦你实现了它,那么单个的逻辑实例将无法拥有独立而不同的行为

解释器模式

使用解释器模式(Interpreter Pattern)为语言创建解释器

当你需要实现一个简单的语言时,使用解释器

当你有一个简单的语法,而且简单比效率更重要时,使用解释器

可以处理脚本语言和编程语言

优点

将每一个语法规则表示成一个类,方便于实现语言

因为语法由许多类表示,所以你可以轻易地改变或扩展此语言

通过在类结构中加入新的方法,可以在解释的同时增加新的行为,例如打印格式的美化或者进行复杂的程序验证

缺点

当语法规则的数目太大时,这个模式可能会变得非常繁杂。在这种情况下,使用解析器或编译器的产生器可能更加合适

中介者模式

使用中介者模式(Mediator Pattern)来集中相关对象之间复杂的沟通和控制方式

中介者常常被用来协调相关的GUI组件

优点

通过将对象彼此解耦,可以增加对象的复用性

通过将控制逻辑集中,可以简化系统维护

可以让对象之间所传递的消息变得简单而且大幅减少

缺点

中介者模式的缺点是,如果设计不当,中介者对象本身会变得过于复杂

备忘录模式

当你需要让对象返回之前的状态时(例如,你的用户请求“撤销”),就使用备忘录模式(MementoPattern)

备忘录用于储存状态

目的

储存系统关键对象的重要状态

维护关键对象的封装

优点

将被储存的状态放在外面,不要和关键对象混在一起,这可以帮助维护内聚

保持关键对象的数据封装

提供了容易实现的恢复能力

缺点

储存和恢复状态的过程可能相当耗时

在Java系统时,其实可以考虑使用序列化(serialization)机制储存系统的状态

原型模式

当创建给定类的实例的过程很复杂时,就使用原型模式(Prototype Pattern)

在一个复杂的类层次中,当系统必须从其中的许多类型创建新对象时,可以考虑原型

优点

向客户隐藏制造新实例的复杂性

提供让客户能够产生未知类型对象的选项

在某些环境下,复制对象比创建新对象更有效

缺点

对象的复制有时相当复杂

访问者模式

当你想要成为一个对象的组合增加新的能力,且封装并不重要时,就使用访问者模式(Visitor Pattern)

当采用访问者模式的时候,就会打破组合类的封装

优点

允许你对组合结构加入新的操作,而无需改变结构本身

想要加入新的操作,相对容易

访问者所进行的操作,其代码是集中在一起的

缺点

因为游走的功能牵涉其中,所以对组合结构的改变就更加困难

定义设计模式

模式是在某情景(context)下,针对某问题的某种解决方案

情境就是应用某个模式的情况。这应该是会不断出现的情况

问题就是你想在某情境下达到的目标,但也可以是某情境下的约束

解决方案就是你所追求的:一个通用的设计,用来解决约束,达到目标

如果你发现自己处于某个情境下,面对这所欲达到的目标被一群约束影响着的问题,然而,你能够应用某个设计,克服这些约束并达到该目标,将你领向某个解决方案

要点

1.让设计模式自然而然地出现在你的设计中,而不是为了使用而使用

2.设计模式并非僵化的教条,你可以依据自己的需要采用或调整

3.总是使用满足需要的最简单解决方案,不管它用不用模式

4.学习设计模式的类目,可以帮你自己熟悉这些模式以及它们之间的关系

5.模式的分类(或类目)是将模式分成不用的族群,如果这么做对你有帮助,就采用吧

6.你必须相当专注才能够成为一个模式的作家,这需要时间也需要耐心,同时还必须乐意做大量的精化工作

7.请牢记:你所遇到大多数的模式都是现有模式的变体,而非新的模式

8.模式能够为你带来的最大好处之一是,让你的团队拥有共享词汇

9.任何社群都有自己的行话,模式社群也是如此。别让这些行话绊着,在读完这本书之后,你已经能够应用大部分的行话了

总结

模式 描述
装饰者 包装一个对象,以提供新的行为
状态 封装了基于状态的行为,并使用委托在行为之间切换
迭代器 在对象的集合之中游走,而不暴露集合的实现
外观 简化一群类的接口
策略 封装可以互换的行为,并使用委托来决定要使用哪一个
代理 包装对象,以控制对此对象的访问
工厂方法 由子类决定要创建的具体类是哪一个
适配器 封装对象,并提供不同的接口
观察者 让对象能够在状态改变时被通知
模板方法 客户用一致的方式处理对象集合和单个对象
组合 客户用一致的方式处理对象集合和单个对象
单件(单例) 确保有且只有一个对象被创建
抽象工厂 允许客户创建对象的家族,而无需指定他们的具体类
命令 封装请求成为对象

并发编程

第一部分

线程安全

1.通过从框架线程中调用应用程序的组件,框架把并发引入了应用程序。组件总是需要访问程序的状态。因此要求在所有代码路径访问状态时,必须时线程安全的
2.设计线程安全的类时,优秀的面向对象技术–封装,不可变性以及明确的不变约束–会给你提供诸多的帮助
3.当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全
4.对于线程安全类的实例进行顺序或并发的一系列操作,都不会导致实例处于无效状态
5.线程安全的类封装了任何必要的同步,因此客户不需要自己提供
6.无状态对象永远是线程安全的
7.为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量
8.对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的
9.每个共享的可变变量都需要由唯一一个确定的锁保护。而维护者应该清楚这个锁
10.对于每个涉及多个变量的不变约束,需要同一个锁保护其所有的变量
11.通常简单性与性能之间是相互牵制的。实现一个同步策略时,不要过早地维克性能而牺牲简单性(这是对安全性潜在的妥协)
12.有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成,执行这些操作期间不要占有锁

共享对象

1.在没有同步的情况下,编译器,处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生在内存中的动作时,你总是会判断错误
2.锁不仅仅是关于同步与互斥的,也是关于内存可见的,为了保证所有线程都能够看到共享的,可变变量的最新值,读取和写入线程必须使用公共的锁进行同步
3.只由当volatile变量能够简化实现和同步策略的验证时,才使用它们。当验证正确性必须推断可见性问题时,应该避免使用volatile变量。正确使用volatile变量的方式包括:用于确保它们所引用的对象状态的可见性,或者用于标识重要的生命周期事件(比如初始化或关闭)的发生
4.枷锁可以保证可见性与原子性,volatile变量只能保证可见性
5.使用volatile变量的基本要求
1.写入变量时并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2.变量不需要与其他的状态变量共同参与不变约束
3.而且,访问变量时,没有其他的原因需要加锁
6.不要让this引用在构造期间逸出
7.不可变对象永远是线程安全的
8.只有满足如下状态,一个对象才是不可变的:
1.它的状态不能在创建后再被修改
2.所有域都是final类型
3.被正确创建(创建期间没有发生this引用的逸出)
9.正如“将所有的域声明为私有的,除非它们需要更高的可见性”一样,“将所有的域声明为final型,除非它们是可变的”,也是一条良好的实践
10.不可变对象可以在没有额外同步的情况下,安全地用于任意线程,甚至发布它们时亦不需要同步
11.为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确创建的对象可以通过下列条件安全地发布:
1.通过静态初始化器初始化对象的引用
2.将它的引用存储到volatile域或AtomicReference
3.将它的引用存储到正确创建的对象的final域中
4.或者将它的引用存储到由锁正确保护的域中
12.任何线程都可以在没有额外的同步下安全地使用一个安全发布的高效不可变对象
13.发布对象的必要条件依赖于对象的可变性
1.不可变对象可以通过任意机制发布
2.高效不可变对象必须要安全发布
3.可变对象必须要安全发布,同时必须要线程安全或者时被锁保护

组合对象

1.设计线程安全类的过程应该包括下面3面基本要素:
1.确定对象状态是由那些变量构成的
2.确定限制状态变量的不变约束
3.制定一个管理并发访问对象状态的策略
2.不理解对象的不变约束和后验条件,你就不能保证线程安全性。要约束状态变量的有效值或者状态转换,就需要原子性与封装性0
3.将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁
4.限制性使构造线程安全的类变得更容易,因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的程序
5.如果一个类由多个彼此独立的线程安全的状态变量组成,并且类的操作不包含任何无效状态转换时,可以将线程安全委托给这些状态变量
6.如果一个状态变量是线程安全的,没有任何不变约束限制它的值,并且没有任何状态转换限制它的操作,那么它可以被安全发布
7.为类的用户编写类线程安全性担保的文档,为类的维护者编写类的同步策略文档

构建块

1.正如封装一个对象的状态,能够使它更加容易地保持不变约束一样,封装它的同步则可以迫使它符合同步策略
2.用并发容器替换同步容器,这种作法以有很小风险带来了可扩展性显著的提高
3.相比于Hashtable和synchronizedMap,ConcurrentHashMap有众多的优势,而且几乎不存在什么劣势,因此在大多数情况下用ConcurrentHashMap取代同步Map实现只会带来更好的可伸缩性。只有当你的程序需要在独占访问中加锁时,ConurrentHashMap才无法胜任(ConcurrentHashMap是线程安全的)
4.有界队列是强大的资源管理工具,用来建立可靠的应用程序,它们遏制那些可以产生过多工作量,具有威胁的活动,从而让你的程序在面对超负荷工作时更加健壮

第一部分总结

1.所有并发问题都归结为如何协调访问并发状态,可变状态越少,保证线程安全就越发容易
2.尽量将域声明为final类型,除非它们的需要是可变的
3.不可变对象天生是线程安全的
4.不可变对象极大地减轻了并发编程的压力。它们简单而且安全,可以在没有锁或者防御性复制的情况下自由地共享
5.封装使管理复杂度变得更可行
6.用锁来守护每一个可变变量
7.对同一不变约束中的所有变量都使用相同的锁
8.在运行复合操作期间持有锁
9.在非同步的多线程情况下,访问可变变量的程序是存在隐患的
10.不要依赖于可以需要同步的小聪明
11.在设计过程中就考虑线程安全。或者在文档中明确地说明它不是线程安全的
12.文档化你的同步策略

第二部分

构建并发应用程序

任务执行

1.如果要在你的程序中实现一个生产者-消费者的设计,使用Executor通常是最简单的方式
2.无论何时当你看到这种形式的代码:
new Thread(runnable).start()
并且你可能最终希望获得一个更加灵活的执行策略时,请认真考虑使用Executor代替Thread
3.Executors中的静态工厂方法

image-20230921090327806

4.大量相互独立且同类的任务进行并发处理,会将程序的任务量分配到不同的任务中,这样才能真正获得性能的提升

取消和关闭

1.在API和语言规范中,并没有把中断与任何取消的语意绑定起来,但是,实际上,使用中断来处理取消之外的任何事情都是不明智的,并且很难支撑起更大的应用
2.调用interrupt并不意味着必然停止目标线程正在进行的工作,它仅仅传递了请求中断的消息
3.中断通常是实现取消最明智的选择
4.因为每一个线程都有其自己的中断策略,所以你不应该中断线程,除非你知道中断对这个线程意味着什么
5.只有实现了线程中断策略的代码才可以接收中断请求。通用目的的任务和库的代码绝不应该接收中断请求
6.对于线程持有的服务,只要服务的存在时间大于创建线程的方法存在的时间,那么就应该提供生命周期方法
7.在一个长时间运行的应用程序中,所有的线程都要给未捕获异常设置一个处理器,这个处理器至少要将异常信息记入日志中

精灵线程

8.线程被分为两种:普通线程和精灵线程(daemon thread)。JVM启动时创建所有的线程,除了主线程以外,其他的都是精灵线程(比如垃圾回收器和其他类似线程)。当一个新的线程创建时,新线程继承了创建它的线程的后台状态,所以默认情况下,任何主线程创建的线程都是普通线程。
9.普通线程和精灵线程之间的差别仅仅在于退出时会发生什么
10.应用程序中,精灵线程不能替代对服务的生命周期恰当,。良好的管理
11.避免使用finalizer

应用线程池

1.一些任务具有这样的特征:需要或者排斥某种特定的执行策略。对其他任务具有依赖性的任务,就会要求线程池足够大,来保证它所依赖任务不必排队或者不被拒绝,采用线程限制的任务需要顺序地执行。把这些需求都写入文档,这样将来的维护者就不会使用一个与原先相悖的执行策略,而破坏安全性或活跃度
2.无论何时,你提交了一个非独立的Executor任务,要明确出现线程饥饿死锁的可能性,并且,在代码或者配置文件以及其他可以配置Executor的地方,任何有关池的大小和配置约束都要写入文档
3.newCachedThreadPool工厂提供了比定长的线程池更好的队列等候性能,它是Executor的一个很好的默认选择。出于资源管理的目的,当你需要限制当前任务的数量,一个定长的线程池就是很好的选择。就像一个接受网络客户端请求的服务器应用程序,如果不进行限制,就会很容易因为过载而遭受攻击。
4.当每个迭代彼此独立,并且完成循环体中每个迭代的工作,意义都足够重大,足以弥补管理一个新任务的开销时,这个顺序循环是适合并行化的

总结

对于并发执行的任务,Executor框架是强大且灵活的。它提供了大量可调节的选项,比如创建和关闭线程的策略,处理队列任务的策略,处理过剩任务的策略,并且提供了几个钩子函数用于扩展它的行为,然而,和大多数强大的框架一样,草率地将一些设定组合在一起,并不能很好地工作;一些类型的任务需要特定的执行策略,而一些调节参数组合在一起后可能产生意外的结果

GUI应用程序

1.Swing的单线程规则:Swing的组件和模型只能在事件分派线程中被创建,修改和请求
2.如果一个数据模型必须被多个线程共享,而且实现一个线程安全模型的尝试却由于阻塞、一致性或者复杂度等原因而失败,这时可以考虑运用分拆模型设计
3.GUI框架几乎都是作为单线程化子系统实现的,所有与表现相关的代码都作为任务在一个事件线程中运行。因为只要唯一一个线程,耗时任务会损害响应性,所以它们应该在后台线程中运行。像SwingWorker以及构建BackgroundTask这些助手类,提供了对取消、进度指示、完成指示的支持/无论是GUI组件还是非GUI组件,都能借助它们简化耗时任务的开发

活跃度,性能和测试

避免活跃度危险

1.安全性和活跃度通常相互牵制。我们使用锁开保证线程安全,但是滥用锁可能引起锁顺序死锁(lock-ordering deadlock)。类似的,我们使用线程池和信号量来约束资源的使用,但是却不能知晓那些管辖范围内的活动可能形成的资源死锁(resource deadlock)。Java应用程序不能从死锁中恢复,所以确保你的设计能够避免死锁出现的先决条件是非常有价值的
2.如果所有线程以通用的固定秩序获得锁,程序就不会出现锁顺序死锁问题了
3.在持有锁的时候调用外部方法是在挑战活跃度问题。外部方法可能会获得其他锁(产生死锁的风险),或者遭遇严重超时的阻塞。当你持有锁的时候会延迟其他试图获得该锁的线程
4.当调用的方法不需要持有锁时,这被称为开放调用
5.在程序中尽量使用开发调用。依赖于开发调用程序,相比于那些在持有锁的时候还调用外部方法的程序,更容易进行死锁自由度(deadlock-freedom)的分析
6.抵制使用线程优先级的诱惑,因为这会增加平台依赖性,并且可能引起活跃度问题。大多数并发应用程序可以对所有线程使用的优先级
7.可伸缩性指的是:当增加计算资源的时候(比如增加额外CPU数量、内存、存储器、I/O带宽),吞吐量和生产量能够相应地得以改进
8.避免不成熟的优化,首先使程序正确,然后再加快–如果它运行得还不够快
9.测评。不要臆测
10.所有得并发程序都要一些串行源;如果你认为你没有,那么去仔细检查吧
11.不要过分担心非竞争得同步带来的开销。基础的机制已经足够快了,在这个基础上,JVM能够进行额外的优化,大大减少或消除了开销。关注那些真正发生了锁竞争的区域中性能的优化
12.串行化会损害可伸缩性,上下文切换回损害性能。竞争性的锁会同时导致这两种损失,所以减少锁的竞争能够改进性能和可伸缩性
13.并发程序中,对可伸缩性首要的威胁是独占的资源锁
14.三种方式来减少锁的竞争
1.减少持有锁的时间
2.减少请求锁的频率
3.或者用协调机制取代独占锁,从而允许更强的并发性
15.Amdahl定律告诉我们,程序的可伸缩性是由必须连续执行的代码比例决定的

总结

16.可伸缩性通常可以通过以下这些方式提升:减少用于获取锁的时间,减小锁的粒度,减少锁的占用时间,或者用非独占或非阻塞锁来取代独占锁

测试并发程序

1.为并发类创建有效的安全测试,其挑战在于:如何在程序出现问题并导致某些属性极度可能失败时,简单地识别出这些检查的属性来,同时不要人为的让查找错误的代码限制住程序的并发性。最好能做到在检查测试的属性时,不需要任何的同步。
2.测试应该在多处理器系统上运行,以提高潜在交替运行的多样性。但是,多个CPU未必会使测试更加高效,为了能够最大程度地检测到时序敏感的数据竞争的发生机会,应该让测试中的线程数多于CPU数,这样在任何给定的时间里,都要一些线程在运行,一些被交换出执行队列,这样可以增加线程间交替行为的随机性
3.编写有效的性能测试,就需要哄骗优化器不要把你的基准测试当作死代码而优化掉。这需要每一个计算的结果都要应用在你的程序中–以一种不需要的同步或真实计算的方式

高级主题

显示锁

1.性能是一个不断变化的目标,昨天的基准显示X比Y更快,这可能已经过时了
2.正如默认的ReentrantLock一样,内部锁没有提供确定的公平性保证,但是大多数锁实现统计上的公平性保证,在大多数条件下已经足够好了,Java语言规范并没有要求JVM公平地实现内部锁,JVM也的确没有这样做。ReentrantLock并没有减少锁的公平性–它只不过使一些存在的部分更显性化了
3.在内部锁不能够满足使用时,ReentrantLock才被作为更高级的工具。当你需要以下高级特性时,才应该使用;可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。否则,请使用synchronized
4.读-写锁(ReadWriteLock)允许多个读者并发访问被守护的对象,当访问多为读取数据结构的时候,它具有改进可伸缩性的潜力

构建自定义的同步工具

1.条件谓词是先验条件的第一站,它在一个操作与状态之间建立起依赖关系
2.将条件谓词和与之关联的条件队列,以及在条件队列中等待的操作,都写入文档
3.每次调用wait都会隐式地与特定的条件谓词相关联。当调用特定条件谓词的wait时,调用者必须已经持有了与条件队列相关的锁,这个锁必须同时还保护着组成条件谓词的状态变量
4.一个单独的内部条件队列可以与多个条件谓词共同使用
5.当使用条件等待(Object.wait或者Condition。await)
1.永远设置一个条件谓词–一些对象状态的测试,线程执行前必须满足它;
2.永远在调用wait前测试条件谓词,并且从wait中返回后再次测试
3.永远在循环中调用wait
4.确保构成条件谓词的状态变量被锁保护,而这个锁正是与条件队列相关联的;
5.当调用wait,notify或者notifyAll时,要持有与条件队列相关联的锁,并且
6.在检查条件谓词之后,开始执行被保护的逻辑之前,不要释放锁
7.无论何时,当你在等待一个条件,一定要确保有人会在条件谓词变为真时通知你
8.只有同时满足下述条件后,才能用单一的notify取代notifyAll(一般使用notifyAll):相同的等待者。只有一个条件谓词与条件队列相关,每个线程从wait返回执行行相同的逻辑;并且,一进一出。一个对条件变量的通知,至多只激活一个线程执行
9.尽管使用notifyAll而非notify可能有些低效,但是这样做更容易确保你的类的行为时正确的
10.一个依赖于状态的类,要么完全将它的等待和通知协议暴露(并文档化)给子类,要么完全阻止子类参与其中
11.危险警告:wait,notify和notifyAll在Condition对象中的对象对等体是await,signal和signalAll。但是,Condition继承Object,这意味着它也有wait和notify方法。一定要确保使用了正确的版本–await和signal!

原子变量与非阻塞同步机制

1.如果能够避免的话,不共享状态的开销会更小。能够通过更有效地竞争改进可伸缩性,但是真正的可伸缩完全是通过减少竞争实现的
2.非阻塞算法通过使用底层级并发原语,比如比较并交换,取代了锁。原子变量类向用户提供了这些底层级原语,也能够当作“更佳的volatile变量”使用,同时提供了整数类和对象引用的原子化更新操作
3.非阻塞算法在设计和实现中很困难,但是在典型条件下能够提供更好的可伸缩性,并能更好地预防活跃度失败。从JVM的一个版本到下一个版本间并发性能的提升很大程度上来源于非阻塞算法的使用,包括在JVM内部以及平台类库。

Java存储模型

1.happens-before的法则包括:
程序次序法制:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都出现在动作A之后
监视器锁法制:对一个监视器锁的解锁happens-before于每一个后续对同一监视器锁的加锁。
volatile变量法制:对volatile域的写入操作happens-before于每一个后续对同一域的读操作。
线程启动法制:在一个线程里,对Thread.start的调用会happens-before于每一个启动程序中的动作
线程启动法制:在一个线程里,对Thread.start的调用会happens-before于每一个启动线程中的动作
线程终结法制:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或者Thread.isAlive返回false。
中断法制:一个线程调用另一个线程的interrupt happens-before 于被中断的线程发现中断(通过抛出InterruptedException,获知调用isInterrupted和interrupted)
终结法制:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
传递性:如果A happens-before于B,且B happens-before 于C,则A happens-before于C
2.除了不可变对象以外,使用被另一个线程初始化的对象,是不安全的,除非对象的发布时happens-before于对象的消费线程使用它
3.初始化安全可以保证,对于正确创建的对象,无论它如何发布的,所有线程都将看到构造函数设置的final域的值,更进一步,一个正确创建的对象中,任何可以通过其final域触及到的变量(比如一个final数组中的元素,或者一个final域引用的HashMap里面的内容),也可以保证对其他线程都是可见的。
4.初始化安全性保证只有以通过final域触及的值,在构造函数完成时才是可见的。对于通过非final域触及的值,或者创建完成后可能改变的值,必须使用同步来确保可见性。

同步Annotation

3个类级Annotation来描述类的可预期的线程安全性保证:@Immutable ,@ThreadSafe 和 @NotThreadSafe

@Immutable自然是意味着类是不可变的,并包含了@ThreadSafe的意义。@NotThreadSafe是可选的–如果类没有被标明是线程安全的,就无法肯定它是不是线程安全的,但是如果你想明确地表示出它不是线程安全的,就标注为@NotThreadSafe

这些Annotation相对是非侵入的,这对用户和维护者都是有益的。用户可以立即看出一个类是否线程安全的,维护者也可以直接检查是否遵循了线程安全性保证。Annotation对于第三个利益既得者也是有用的:工具。静态的代码分析工具可以有能力对代码进行验证,看它是否遵循了由Annotation指定的契约,比如标明为@Immutable的类是否真是不可变的

JAVA建议

1.考虑用静态工厂方法代替构造函数

静态工厂方法的好处

1.与构造函数不同,静态工厂方法具有名字

2.与构造函数不同,它们每次被调用的时候,不要求非得创建一个新的对象

3.与构造函数不同,它可以返回一个原返回类型的子类型的对象

缺点

主要:类如果不含公有的或者受保护的构造函数,就不能被子类化

2.它们与其他的静态方法没有任何区别

2.使用私有构造函数强化singleton属性

3.通过私有构造函数强化不可实例化的能力

4.避免创建重复的对象

5.消除过期的对象引用

6.避免使用终结函数

7.在改写equals的时候请遵守通用约定

8.在改写equals时总是要改写hashCods

9.总是要改写toString

10.谨慎改写clone

11.考虑实现Comparable接口

12.使类和成员的可访问能力最小化

尽可能使每一个类或成员不被外界访问

13.支持非可变性

1.不要提供任何会修改对象的方法(也称为mutator)
2.保证没有可被子类改写的方法
3.使所有的域都是final的
4.使所有域都是成为私有的
5.保证对于任何可变组件的互斥访问

14.复合优先于继承

15.要么专门为继承而设计,并给出文档说明,要么禁止继承

一个类必须通过某种形式提供适合的钩子,以便能够进入到它的内部工作流程中,这样的形式可以是精心选择的受保护(protected)方法

构造函数一定不能调用可被改写的方法

无论是clone还是readObject,都不能调用一个可改写的方法,不管是直接的方式,还是间接的方式

为了继承设计一个类,要求对这个类有一些实质性的限制

对于那些并非为了安全地进行子类化而设计和编写文档类,禁止子类化

禁止子类化的两种方法

1.直接把这个类声明为final的

2.把所有的构造函数变成私有的,或者包级私有的,并且增加一些公有的静态工厂来替代构造函数的位置

16接口优于抽象类

接口和抽象类最大的区别是:抽象类允许包含某些方法的实现,但是接口是不允许的

已有的类可以很容易被更新,已实现新的接口
接口是定义mixin(混合类型)的理想选择
接口使得我们可以构造出非层次结构的类型框架
接口使得安全地增强一个类的功能成为可能

你可以把接口和抽象类的优点结合起来,对于你期望导出的每一个重要接口,都提供一个抽象的骨架实现(skeletal implementation)类

抽象类的演化比接口的演化要容易得多

17.接口只是被用于定义类型

常量接口模式是对接口的不良使用

18.优先考虑静态成员类

如果你声明的成员类不要求访问外围实例,那么请记住把static修饰符放到成员类的声明中

19.用类代替结构

20.用类层次来代替联合

21.用类来代替enum结构

22.用类和接口来代替函数指针

23.检查参数的有效性

24.需要时使用保护性拷贝

假设类的客户会尽一切手段来破坏这个类的约束条件,在这样的前提下,你必须保护性地设计程序
对于构造函数的每个可变参数进行保护性拷贝(defensive copy)是必要的
保护性拷贝动作是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是原始的对象
对于“参数类型可以被不可信方子类化”的情形,请不要使用clone方法进行参数的保护性拷贝

25.谨慎设计方法的原型

谨慎选择方法的名字
不要过于追求提供便利的方法
避免长长的参数列表
对于参数类型,优先使用接口而不是类
谨慎地使用函数对象

26.谨慎地使用重载

对于重载该方法(overloaded method)的选择是静态的,而对于被改写的方法(overridden method)的选择是动态的
避免方法重载机制的混淆用法
一个安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法

27.返回零长度的数组而不是null

没有理由从一个取数组值(array-valued)的方法中返回null,而不是返回一个零长度数组

28.为所有导出的API元素编写文档注释

为了正确地编写API文档,你必须在每一个被导出的类,接口,构造函数,方法和域声明之前增加一个文档注释
每一个方法的文档注释应该简洁地描述出它和客户之间的约定

29.将局部变量的作用域最小化

使一个局部变量的作用域最小化,最有力的技术是在第一次使用它的地方声明
几乎每一个局部变量的声明都应该包含一个初始化表达式

30.了解和使用库

通过使用标准库,你可以充分利用这些编写标准库的专家的知识,以及在你之前其他人的使用经验
在每一个主要的发行版本中,都会有许多新的特性被加入到库中,所以与这些库保持同步是值得的

31.如果要求精确的答案,请避免使用float和double

32.如果其他类型更合适,则尽量避免使用字符串

字符串不适合代替其他的值类型
字符串不适合代替枚举类型
字符串不适合代替聚集类型
字符串也不适合代替能力表

33.了解字符串连接的性能

为连接n个字符串而重复地使用字符串连接操作符,要求n的平方级的时间
为了获得可接受的性能,请使用StringBuffer替代String

34.通过接口引用对象

如果你养成了使用接口作为类型的习惯,那么你的程序将会更加灵活
如果没有合适的接口存在的话,那么,用类而不是接口来引用一个对象,是完全合适的

35.接口优先于映像机制

映像机制的代价
损失了编译时类型检查的好处
要求执行映像访问的代码非常笨拙和冗长
性能损失
通常,普通应用在运行时刻不应该以映像方式访问对象
如果只是在很有限的情况下使用映像机制,那么虽然也会付出少许代价,但你可以获得许多好处

36.谨慎地使用本地方法

37.谨慎地进行优化

努力避免那些限制性能的设计决定
考虑你的API设计决定的性能后果
为了获得好的性能而对API进行曲改,这是一个非常不好的想法
在每次试图做优化之前和之后,请对性能进行测量

39.只针对不正常的条件才使用异常

38.遵守普遍接受的命名惯例

异常只应该被同于不正常的条件,它们永远不应该被用于正常的控制流
一个设计良好的API不应该强迫它的客户为了正常的控制流而使用异常

40.对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常

如果期望调用者能够恢复,那么,对于这样的条件应该使用被检查的异常
用运行时异常来指明程序错误
你所实现的所有的未被检查的抛出结构都应该时RuntimeException的子类(直接的或者间接的)

41.避免不必要地使用被检查的异常

42.尽量使用标准的异常

43.抛出的异常要适合于相应的抽象

高层的实现应该捕获底层的异常,同时抛出一个可以按照高层抽象进行解释的异常
尽管异常转译比不加选择地传递低层异常的做法有所改进,但是它也不能被滥用

44.每个方法抛出的异常都要有文档

总是要单独地声明被检查的异常,并且利用Javadoc的@throws标记,标准地记录下每个异常被抛出的条件
使用Javadoc的@throws标签记录下一个方法可能会抛出的每个未被检查的异常,但是不要使用throws关键字将未被检查的异常包含在方法的声明中
如果一个类中的许多方法出于同样的原因而抛出同一个异常,那么在该类的文档注释中对这个异常做文档,而不是为每个方法单独做文档,这是可以接受的

45.在细节消息中包含失败-捕获消息

为了捕获失败,一个异常的字符串表示应该包含所有“对该异常有贡献”的参数和域的值

46.努力使失败保持原子性

一般而言,一个失败的方法调用应该使对象保持“它在被调用之前的状态”

47.不要忽略异常

空的catch块会使异常达不到应有的目的
至少catch块也应该包含一条说明,用来解释为什么忽略掉这个异常是适合适的

48对共享可变数据的同步访问

为了提高性能,在读或写原子数据的时候,你应该避免使用同步。这个建议是非常危险而错误的
为了在线程之间可靠地通信,以及为了互斥访问,同步是需要的
一般情况下,双重检查模式并不能正确地工作
简而言之,无论何时当多个线程共享可变数据的时候,每个读或者写数据的线程必须获得一把锁

49.避免过多的同步

为了避免死锁的危险,在一个被同步的方法或者代码块中,永远不要放弃对客户的控制

50.永远不要在循环的外面调用wait

总是使用wait循环模式来调用wait方法

51.不要依赖于线程调度器

任何依赖于线程调度器而达到正确性或性能要求的程序,很有可能是不可移植的
线程优先级是Java平台上最不可移植的特征了
对于大多数程序员来说,Thread.yield的惟一用途是在测试期间人为地增加一个程序的并发性

52.线程安全性的文档化

在一个方法的声明中出现synchronization修饰符,这是一个实现细节,并不是实现细节,并不是导出的API的一部分
一个类为了可破多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别
安全级别

非可变的

线程安全的

有条件的线程安全

线程兼容的

线程对立的

53.避免使用线程组

线程组基本上已经过时了

54.谨慎地实现Serialization

因为实现Serialization而付出的最大代价是,一旦一个类被发布,则“改变这个类的实现”的灵活性将大大降低
实现Serialization的第二个代价是,它增加了错误(bug)和安全漏洞的可能性
实现Serialization的第三个代价是,随着一个类的新版本的发行,相关的测试负担增加了
实现Serialization接口不是一个很轻松就可以做出的决定
为了继承而设计的类应该很少实现Serialization,接口也应该很少会扩展它
对于为继承而设计的不可序列化的类,你应该考虑提供一个无参数的构造函数

55.考虑使用自定义的序列化形式

若没有认真考虑默认序列化形式是否合适,则不要接受这种形式
如果一个对象的物理表示等同于它的逻辑内容,则默认的序列化形式可能是合适的
即使你确定了默认序列化形式是合适的,通常你仍然要提供一个readObject方法以保证约束关系和安全性
当一个对象的物理表示与它的逻辑数据内容有实质性的区别时,使用默认序列化形式有4个缺点:

1.它使这个类的导出API永远地束缚在该类的内部表示上

2.它要消耗过多的空间

3.它要消耗过多的时间

4.它会引起栈溢出

transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉
如果所有的实例域都是transient的,那么,从技术角度而言,省去调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这么做
在决定将一个域做成非transient之前,请一定要确信它的值将是该对象逻辑状态的一部分
不管你选择了那种序列化形式,你都要为自己编写的每个可序列化的类声明一个显式的序列版本UID(serial version UID)

56.保护性地编写readObject方法

当一个对象被反序列化的时候,对于客户不应该拥有的对象引用,如果哪个域包含了这样的对象引用,则必须要做保护性拷贝,这是非常重要的

57.必要时提供一个readResolve方法

readResolve方法不仅仅对于singleton对象是必要的,而且对于所有其他的实例受控的(instance-controlled)类也是必需的
readResolve方法的第二个用法是,就像在第56条中建议的那样,作为保护性的readObject方法的一种保守的替代选择
尽管保护性readResolve模式并没有被广泛使用,但是它值得认真考虑
readResolve方法的可访问性(accessibility)是非常重要的
  • Copyrights © 2015-2026 Immanuel
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

微信