66.同步访问共享的可变数据
简介
- 关键字
synchronized
可以保证在同一时刻,只有一个线程可以执行某一个方法.或者某一代码块 - Java 语言规范保证
读或者写
一个变量是原子性
的,除非这个变量的类型是long
或者double
- 为了在线程之间进行可靠的通信,也为了互斥访问,同步是必须的.
volatile
修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候都将看到最近杠杆被写入的值.- 使用
volatile
需要格外谨慎,因为它并没有互斥作用,如果声明一个volatile int
,然后对其进行++
操作,那将会导致data corruption
,因为++
不是原子性的. - 要么不共享数据,要么共享不可变数据,将可变数据限制在单个线程中.
小结
- 当
多个线程
共享可变数据
的时候每个读
或者写数据
的线程都必须执行同步
. volatile
是一种可接受的同步形式
67.避免过度同步
简介
- 过渡同步可能导致
性能降低
,死锁
甚至不确定
的行为. - 为了避免
活性失败和安全性失败
,在一个被同步的方法
或者代码块
中,永远不要放弃对客户端的控制
,即,在一个被同步的区域内部,不要滴啊用设计成要被覆盖的方法. - 如果要将外来方法的调用移出同步的代码块,可以使用
CopyOnWriteArrayList
- 通常应该在同步区域内做尽可能少的工作.
StringBuffer
几乎总是被用于单个线程之中,但是他们执行的确是内部同步
因此被StringBuilder
代替.
小结
- 为避免死锁和数据破坏,不要在同步区域内部调用外来方法.
- 要尽量限制同步区域内的工作量.
- 当设计一个可变类时,应该考虑他们是否应该自己完成同步操作.
68.executor和task优先于线程
简介
Executors
提供了多个工厂方法,创建ExecutorService
,还可以直接使用ThreadPoolExecutor
,对线程池做更精细的控制.- 如果
轻负载
,使用Executors.newCachedThreadPool
,如果重负载,用Executors.newFixedThreadPool
- 任务和机制被分别抽象了,前者为
Runnable
和Callable
,后者则是executor service
; java.util.Timer
是单线程,且抛出未捕获异常
会终止,可以使用ScheduledThreadPoolExecutor
替代;
69.并发工具优先于wait和notify
java.util.concurrent包主要包含三块:
- Executor Framework(任务执行工具)
- concurrent collections(并发集合)
- synchronizers(同步器)
concurrent collections
concurrent collections
提供了标准容器的高性能并发实现.内部同步和互斥,外部使用,无需加锁.- 优先使用
ConcurrentHashMap
,而不是Collections.synchronizedMap
或者Hashtable
,且无需做同步操作.- 有的
concurrent collections
提供了block
操作接口,例如BlockingQueue
,从中取数据的时候,如果队列为空,线程将等待,新的数据加入后,将自动唤醒等待的线程;大部分的ExecutorService
都是采用这种方式实现的
synchronizers
最常用的同步器是CountDownLatch
和Semaphore
,较不常用的是CyclicBarrier
和Exchanger
CountDownLatch
允许多个线程
等待另外一个
或多个线程
完成某种工作- 如果非要用
wait
和notify
,注意以下几点:
wait
前的条件检查,当条件成立时,就跳过等待,可以保证不会死锁,wait
后的检查,条件不成立继续等待,可以保证安全- 通常情况下都应该使用
notifyAll
,虽然从优化角度看,这样不好.
建议
- 对于
间歇式
定时,应该始终使用System.nanoTime
而不是System.cucurrentTimeMills
- 应该始终使用
wait
循环模式来调用wait
方法.不要在循环外调用wait
方法.
小结
- 直接使用
wait
和notify
,就像 用并发汇编语言
进行编程一样.而concurrent
则提供了更高级的语言 - 没有理由在新代码中使用
wait
和notify
,即使有,也很少 - 如果正在维护使用
wait
和notify
的代码,则尽量在while
循环内部调用wait
- 应该优先使用
notifyAll
,而不是notify
.
70.线程安全性的文档化
简介
- 一个方法的声明中加了
synchronized
并不能保证它是线程安全的,并且Javadoc也不会把这个关键字输出到文档中 - 一个类为了可被多个线程安全的使用,文档中必须清楚的说明它所支持的
线程安全级别
线程安全级别
- 不可变的 — 这个类的实例是不可变得
- 无条件的线程安全 — 这个类的实例是可变的,但是有着足够的内部同步,类的实例
无需外部同步即可被并发使用
- 有条件的线程安全 — 有些方法为进行安全的并发,需要使用外部同步
- 非线程安全 — 这个类的实例是可变的.为了并发的使用它们,客户端必须利用自己选择的外部同步包围方法调用.
- 线程对立的 — 这个类不能安全地被多个线程并发的使用,即使所有的方法被外部同步包围
小结
- 当一个类承诺了
使用一个公有可访问的锁对象
时,就意味着允许客户端以原子的方式执行一个方法的调用序列. 但是这种方法会发生拒绝服务攻击
,只要客户端超时地持有公有可访问锁
即可.- 这里和
13条中
,使类和成员的可访问性最小化不谋而合,要把锁对象封装在它所同步的对象中.lock
域应被声明为final
,防止不小心改变它的内容.私有锁
只能用在无条件的线程安全
类上.适用于专为继承而设计的类
71.慎用延迟初始化
简介
- 延迟到需要 域的值时 才将它初始化的行为,如果永远不需要,就永远不会被初始化.
- 和大多数优化一样,建议
除非必要,否则不要这么做
- 如果域只在类的实例部分被访问,并且初始化开销很高,就值得延迟初始化
- 大多数情况,正常的初始化要优先于延迟初始化.
- 如果静态成员出于性能考虑需要延迟初始化,就使用
lazy initialization holder class idiom
- 出于性能考虑对实例进行延迟初始化,要使用
double-check idiom
(使用volatile
关键字) - 如果不在意是否为每个线程都重新计算域的值,且域类型为基本类型(不是
long
或者double
),可以删除volatile
关键字
小结
简而言之,大多数的域应该正常的进行初始化,否则,可以参考上面的规则,进行延迟初始化
72.不要依赖于线程调度器
简介
- 当有
多个线程
可以运行时,由线程调度器
决定哪些线程将会执行.以及运行多长时间- 任何
依赖于线程调度器
来达到正确性或者性能要求
的程序,很有可能都是不可移植的.- 要确保
可运行线程
的平均数量不
明显多于处理器
的数量
保持可运行线程数量尽可能少
- 让每个线程做些有意义的工作
- 如果 线程没有做有意义的工作.就不应该运行
- 不要企图使用
Thread.yield
来修正
多线程,因为Thread.yield
没有可测试的语义 - 线程优先级是
Java
平台中移植性最差的部分,所以也不要用 Thread.yield
的唯一用途,就是在测试期间人为的增加程序的并发性
小结
- 不要让应用程序的并发性依赖于线程调度器
- 不要依赖
Thread.yield
和线程优先级
73.避免使用线程组
简介
- 除了
线程
,锁
和监视器
之外,系统还提供了一个基本的抽象
: 线程组(Thread Group
). ThreadGroup
是为了方便线程管理出现的,可以统一设定线程组的一些属性.线程组
可有子线程
和子线程组
.ThreadGroup
批量管理线程.