Fork me on GitHub

并发编程

第一部分

线程安全

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
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

微信