《Java 并发编程实战》读书笔记,待更新…
资源
官方网站(包含了勘误表、书中源码等文档和资源)
这篇豆瓣书评提到了本书的优缺点,蛮不错的,可以一看。
目录
第 1 章 简介
P5
虽然递增运算
someVariable++
看上去是单个操作,但事实上它包含三个独立的操作:读取 value,将 value 加 1,并将计算结果写入 value。这里讲到了一个操作的原子性。原子操作是不可再分割的操作,要么执行,要么不执行。而
someVariable++
是一个复合操作,包含了三个原子操作。
第一部分 基础知识
第 2 章 线程安全性
P12
但在编写并发应用程序时,一种正确的编程方法就是:首先使代码正确运行,然后再提高代码速度。即便如此,最好也只是当性能测试结果和应用需求告诉你必须提高性能,以及测量结果表明这种优化在实际环境中确实能带来性能提升时,才进行优化。
Donald Knuth 也说过“过早优化是万恶之源”(premature optimization is the root of all evil)。
第 3 章 对象的共享
P29
Java 内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非 volatile 类型的 long 和 double 变量,JVM 允许将 64 位的读操作或写操作分解为两个 32 位的操作。
关于这一点,廖雪峰 Java 教程的这一节:线程同步 文章最后小结前的部分也有讲到。而且讲的更清晰易懂。
P31
当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。
volatile 是确保可见性的,并不能保证操作的原子性,这点要搞清楚。
可见性:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
加锁机制是既可以确保可见性又可以确保原子性的。
详细请看廖雪峰 Java 教程这篇 中断线程 文章最后小结前的部分。P34
不要在构造过程中使 this 引用逸出。
P38
当满足以下条件时,对象才是不可变的:
- 对象创建以后其状态就不能修改。
- 对象的所有域都是 final 类型。
- 对象是正确创建的(在对象的创建期间,this 引用没有逸出)。
P38 底部注释
从技术上看,不可变对象并不需要将其所有的域都声明为 final 类型,例如 String 就是这种情况,这就要对类的良性数据竞争(Benign Data Race)情况做精确分析,因此需要深入理解 Java 内存模型。(注意:String 会将散列值的计算推迟到第一次调用 hash Code 时进行,并将计算得到的散列值缓存到非 final 类型的域中,但这种方式之所以可行,是因为这个域有一个非默认的值,并且在每次计算中都得到相同的结果[因为基于一个不可变的状态]。自己在编写代码时不要这么做。)
这里讲到了 String 类的不可变性,String 类中的
hash
字段虽然是非 final 的,但还是可以保证其不变性。P42
由于不可变对象是一种非常重要的对象,因此 Java 内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。另一方面,即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。
这种 Java 内存模型为不可变对象提供的特殊初始化安全性保证机制,书中这里没有详细讲解,也许在《深入理解 Java 虚拟机》里会有。
P43
静态初始化器由 JVM 在类的初始化阶段进行。由于在 JVM 内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。
第 4 章 对象的组合
- P51
在许多类中都使用了 Java 监视器模式,例如 Vector 和 Hashtable。Java 监视器模式的主要优势就在于它的简单性。
第 5 章 基础构建模块
P70
容器的 hashCode 和 equals 等方法也会间接地执行迭代操作,当容器作为另一个容器的元素或键值时,就会出现这种情况。同样,containsAll、removeAll 和 retainAll 等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。所有这些间接的迭代操作都可能抛出 ConcurrentModificationException。
还有书上举例的对容器打印时调用的 toString 函数,也会对容器进行迭代(这里勘误表p.83 有补充,不过我没太看懂。。)
P71
ConcurrentHashMap 并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁(Lock Striping,请参见 11.4.3 节)。在这种机制中,任意数量的读取线程可以并发地访问 Map,执行读取操作的线程和执行写入操作的线程可以并发地访问 Map,并且一定数量的写入线程可以并发地修改 Map。ConcurrentHashMap 带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。
书中这段对 ConcurrentHashMap 的特点概括非常精彩,提到了它在并发环境下的设计权衡:用处很小的 size 和 isEmpty 返回的值不一定准确,因为它们的返回值总在不断变化,但是换取了 get、put、containsKey 和 remove 等更重要操作的性能优化。
P74
开发人员总会假设消费者处理工作的速率能赶上生产者生成工作项的速率,因此通常不会为工作队列的大小设置边界,但这将导致在之后需要重新设计系统架构。因此,应该尽早地通过阻塞对了在设计中构建资源管理机制——这件事情做得越早,就越容易。在许多情况下,阻塞队列能使这项工作更加简单,如果阻塞队列并不完全符合设计需求,那么还可以通过信号量(Semaphore)来创建其他的阻塞数据结构(请参见 5.5.3 节)。
P77
Java 6 增加了两种容器类型,Deque(发音为 “deck”)和 BlockingDeque
我之前一直读作 “滴Q” ,汗。。。
P79
等待直到某个操作的所有参与者(例如,在多玩家游戏中的所有玩家)都就绪再继续执行。在这种情况中,当所有玩家都准备就绪时,闭锁将到达结束状态。
比如王者打匹配要等所有玩家都确认之后才会进入选择英雄界面?
P83
栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
闭锁用于所有线程等待一个外部事件的发生;栅栏则是所有线程相互等待,直到所有线程都到达某一点时才打开栅栏,然后线程可以继续执行。参考来源
第二部分 结构化并发应用程序
本部分包括之后的部分,看书的时候大概能看懂,但由于是实践部分,所以要在实际代码中回来反复看反复印证。而且,很多细节,没有实际代码用到,体会不到。
第 6 章 任务执行
P97
改变 Executor 实现或配置所带来的影响要远远小于改变任务提交方式带来的影响。通常,Executor 的配置是一次性的,因此在部署阶段可以完成,而提交任务的代码却会不断地扩散到整个程序中,增加了修改的难度。
P98
每当看到下面这种形式的代码时:
new Thread(runnable).start()
并且你希望获得一种更灵活的执行策略时,请考虑使用 Executor 来代替 Thread。P99 底部注释
但在足够长的时间内,如果任务到达的速度总是超过任务执行的速度,那么服务器仍有可能(只是更不易)耗尽内存,因为等待执行的 Runnable 队列将不断增长。
P101 讲了 Timer 类需要注意的坑点,以后用的到的话可以看一看。
P106
只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。
第 7 章 取消与关闭
P115
通常,中断是实现取消的最合理方式。
P117
只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该屏蔽中断请求。