读书笔记: Java并发编程实战

https://book.douban.com/people/fleure/annotation/10484692/

避免热点域

<原文开始>当每个操作都请求多个变量时,锁的粒度将很难降低。这是在性能与可伸缩性之间相互制衡的另一个方面,一些常见的优化措施,例如将一些反复计算的结果缓存起来,都会引入一些“热点域”,而这些热点域往往会限制可伸缩性。</原文结束> ## 资源死锁 <原文开始>资源池通常采用信号量来实现。</原文结束> <原文开始>如果某些任务需要等待其它任务的结果,那么这些任务往往是产生线程饥饿死锁的主要来源,有界线程池/资源池与相互依赖的任务不能一起使用。</原文结束> goroutine 里面相互依赖的任务该挺多吧,而且也是有界线程池? ## 开放调用 <原文开始>如果在调用某个方法时不需要持有锁,那么这种调用称为开放调用。依赖于开放调用的类通常能表现出更好的行为,并且与那些正在调用方法时需要持有锁的类相比,也更容易编写。</原文结束> <原文开始>分析一个完全依赖于开放调用的程序的活跃性,要比分析那些不依赖开放调用的程序的活跃性简单。</原文结束> <原文开始>在重新编写同步代码块以使用开放调用时会产生意想不到的结果,因为这会使得某个原子操作变为非原子操作。在许多情况下,使某个操作失去原子性是可以接收的。</原文结束> 是说 synchronized 关键字用在业务代码的方法上会很坑么。为了避免死锁,方法的原子性有时候是可以牺牲的。慎用 synchronized 方法,除非只跟私有字段打交道。 ## 在协作对象之间发生的死锁 <原文开始>在 LeftRightDeadLock 或 transferMoney 中,要查找死锁是比较简单的,只需要找出那些需要获得两个锁的方法。然而要在 Taxi 和 Dispatcher 中查找死锁则比较困难:如果在持有锁的情况下调用某个外部方法,那么就需要警惕死锁。</原文结束> 在持有锁的时候不能调用外部方法,可不可以通过静态分析来防止呢。 ## 动态的锁顺序死锁 <原文开始>有时候,并不能清楚地知道是否在锁顺序上有足够的控制权来避免死锁的发生。</原文结束> <代码开始 lang="java"> public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException { synchronized (fromAccount) { synchronized (toAccount) { if (fromAccount.getBalance().compareTo(amount) < 0) { fromAccount.debit(amount); toAccount.credit(amount); } } } } </代码结束> 锁定对象的顺序来自于外部参数,使上锁的顺序不可控。解决方案是对 fromAccount 和 toAccount 进行排序,按唯一顺序上锁。C 的话直接按指针排序就行了,似乎也是 kernel 的做法。 ## 管理队列任务 <原文开始>如果线程池较小而队列较大,那么有助于减少内存使用量,降低 CPU 的使用率,同时还可以减少上下文切换,但付出的代价是可能会限制吞吐量。</原文结束> <原文开始>只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能导致线程“饥饿”死锁问题。此时应该使用无界的线程池。</原文结束> ## 设置线程池的大小 <原文开始>对于计算密集的任务,在拥有 N 个处理器的系统上,当线程池大小为 N+1 时,通常能实现最优的利用率。(即使当计算密集型的线程偶尔由于页缺失故障或者其它原因而暂停时,这个“额外”的线程也能确保 CPU 的时钟周期不会被浪费。)对于包含 I/O 操作或者其它阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。</原文结束> <原文开始>计算每个任务对该资源的需求量,然后用该资源的可用总量除以每个任务的需求量,所得结果就是线程池大小的上限。</原文结束> <原文开始>当任务需要某种通过资源池来管理的资源时,例如数据库连接,那么线程池和资源池的大小将会相互影响。如果每个任务都需要一个数据库连接,那么连接池的大小就限制了线程池的大小。同样,当线程池中的任务是数据库连接的唯一使用者时,那么线程池的大小又将限制连接池的大小。</原文结束> ## 中断策略 <原文开始>对于非线程所有者的代码来说,应该小心地保存中断状态,这样拥有线程的代码才能对中断做出响应。</原文结束> <原文开始>批评者嘲笑 java 的中断功能,因为它没有提供抢占式的中断机制,而且还强迫开发人员必须处理 InterruptedException。然而,通过推迟中断请求的处理,开发人员能定制更灵活的中断策略,从而使程序在响应性和健壮性之间实现合理的平衡。</原文结束> ## 使用 CompletionService 实现页面渲染器 <原文开始>因此,CompletionService 的作用就相当于一组计算的句柄,这与 Future 作为单个计算的句柄是非常相似的。</原文结束> ## 双端队列和工作密取 <原文开始>正如阻塞队列适用于生产者-消费者模式,双端队列同样适用于另一种相关模式,即工作密取(Work Stealing)。在生产者-消费者设计中,所有消费者有一个共享的工作队列,而在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己的双端队列中的全部工作,那么它可以从其它消费者双端队列末尾秘密地获取工作。密取工作模式比传统的生产者-消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数时候,它们都只是访问自己的双端队列,从而极大地减小了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是头部获取工作,因此进一步降低了队列上的竞争程度。</原文结束> <原文开始>工作密取非常适用于既是消费者也是生产者问题—当执行某个工作时可能导致出现更多的工作。例如,在网页爬虫程序在处理一个页面时,通常会发现有更多的页面需要处理。</原文结束> ## 不变性 <原文开始>当满足以下条件时,对象才是不可变的: - 对象创建以后其状态就不可修改 - 对象的所有域都是 final 类型 - 对象时正确创建的(在对象的构造期间,this 引用没有逸出) </原文结束> <原文开始>从技术上来看,不可变对象并不需要将其所有的域都声明为 final 类型,例如 String 就是这种情况,这就要对类的良性数据竞争情况做精确的分析,因此需要深入理解 Java 的内存模型。... 自己在编码时不要这么做。</原文结束> 没错,内存模型并不是为业务程序员准备的。Doug Lea 给你警告了,不要自大。 ## 线程封闭 <原文开始>一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术称为线程封闭,它是实现线程安全的最简单方式之一。</原文结束> <原文开始>在 Swing 中大量使用了线程封闭技术。Swing 的可视化组件和数据模型对象都不是线程安全的,Swing 通过将它们封闭到 Swing 的事件分发线程中来实现线程安全性。</原文结束> Cocoa 的 Grand Central Dispatch 该也是类似的技术。 <原文开始>线程封闭技术的另一种常见应用是 JDBC 的 Connection 对象。JDBC 规范并不要求 Connection 对象必须是线程安全的。在典型的服务器应用程序中,线程从连接池中获得一个 Connection 对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。... 这种连接管理模式在处理请求时隐含地将 Connection 对象封闭在线程中。</原文结束> ## 发布与逸出 <原文开始>无论其她的线程会对已发布的引用执行何种操作,其实都不重要,因为误用该引用的风险始终存在。 如果有人窃取了你的密码并发布到 alt.passwords 新闻组上,那么你的信息将 “逸出”:无论是否有人会恶意地使用这些个人信息,你的账户都已经不再安全了。发布一个引用同样会带来类似的风险。 </原文结束> 闭包的问题就在于太容易逸出变量了,而且很难控制。 ## volatile 变量 <原文开始>加锁机制既可以确保可见性又可以确保原子性,而 volatile 变量只能确保可见性。</原文结束> <原文开始>当且仅当满足以下所有条件时,才应该使用 volatile 变量: - 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。 - 该变量不会与其它状态变量一起纳入到不变性条件中。 - 在访问变量时不需要加锁。 </原文结束> ## 线程安全性 <原文开始>当执行时间较长的计算或者可能无法快速完成的操作(例如网络IO或控制台IO)时,一定不要持有锁。</原文结束> ## 线程安全性 <原文开始>在一些大型程序中,要找出多个线程在哪些位置上将访问同一个变量是非常复杂的。幸运的是,面向对象这种技术不仅有助于编写出结构优雅、可维护性高的类,还有助于编写出线程安全的类。访问某个变量的代码越少,就越容易确保对变量的所有访问都实现正确的同步,同时也更容易找出变量再哪些条件下被访问。</原文结束> <原文开始>程序的封装性越好,就越容易实现程序的县城安全性,并且代码的维护人员也越容易保持这种方式。</原文结束> <原文开始>完全由线程安全类构成的程序不一定是线程安全的,而在线程安全类中也可以包含非线程安全的类。</原文结束> <原文开始>正确性的含义是指,某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及定义各种后验条件(Postconditions)来描述对象操作的结果。</原文结束> <原文开始>任何情况下,只有当类中仅包含自己的状态时,线程安全类才是有意义的。</原文结束> <原文开始>通常,线程安全性的需求并非来源于对线程的直接使用,而是使用像 Serverlet 这样的框架。</原文结束> <原文开始>最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失败的观测结果来决定下一步的动作。</原文结束>