Upload
dai-jun
View
2.615
Download
2
Embed Size (px)
Citation preview
Notes of JCIP
by guiwuu
from 2011-01-27 to 2011-03-22
JCIP主要内容
• 并发编程的基础概念
• 并发编程的设计原则
• Java并发编程API(包括j.u.c)
• Java并发程序的测试和性能调优
• Java并发编程背后的JMM、JVM和硬件等
为什么要并发编程
• 摩尔定律向Amdahl定律转换
• 多核处理器飞入寻常百姓家
• multi processor, multi-core都属于并行计算
为什么要多线程编程
• 为什么要多进程?资源利用、公平和方便
• 多进程:不同任务不同进程独立执行
• 多线程:任务分解子任务并行执行
• 进程,轻量级进程,线程
• 进程间通信手段:socket,signal,shared memory,semaphores,file
• Thread是cpu调度的单位
多线程的优点
• 提高资源利用率:多CPU,IO
• 简化模型,如Servlet、RMI等框架
• 简化编程,化异步为同步,例如多线程同
步的IO比自己做NIO要容易得多
• 提高程序响应性,特别是GUI程序
• 提高系统性能、吞吐量
多线程的风险(常见bug)
• 安全风险
– 不安全的发布(内存可见性问题)
– race condition
– data race
• 活跃度失败,liveness failure
– 死锁,饿死,活锁
• 额外的性能开销
– cpu调度,上下文切换,锁开销
编写多线程程序安全是第一位的
什么是多线程安全
• 定义:当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和变替执行,并且不需要额外的同步及在调用方式代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的
• 正确性,意味着一个类与它的规约保持一致。良好的规约定义了用于强制对象状态的不变约束以及描述操作影响的后验条件
• 安全性,指在多个线程访问时,类可以持续进行正确的行为
怎么做到多线程安全
• OO的封装很重要,是线程安全类的必要条件
• 编写线程安全的代码,本质是管理对状态的访问,而且通常都是共享的、可变的状态– 状态,就是存储在状态变量里的数据,状态变量如对象的静态域、私有域以及附属对象
– 共享,一个变量可被多个线程访问
– 可变,变量值在其生命周期可能改变
• 要么stateless,要么immutable,要么同步一切状态变量的访问(包括读写!)– Stateless,如Servlet单实例可同时响应多个请求
– 可以采用线程限制、同步、原子操作等手段避免竞争条件
原子性
• 单独的不可分割的操作,与db事务类似
• 原子操作
– 对应一条CPU指令,如
• 读写非64位的原语变量或对象引用
• Compare and Swap操作,CAS
– 锁具有原子性
• race condition会破坏原子性
• 复合操作也需要原子性
竞争条件race condition
• 当计算的正确性依赖于运行时特定的时序
或多线程交替执行时就会产生竞争条件
• 竞争条件不同于数据竞争data race,虽然它
们都会导致并发失败
• 常见的竞争条件
– Read-modify-write,如自增操作++I
– Check-then-act,如延迟技术,lazy initialization
– 复合操作,如ConcurrentHashMap.putIfAbsent
– 不安全的发布
同步
• 广义来说,包含了互斥性、原子性(包括顺序性)、可见性三个方面
• 可以用来阻止竞争条件,但是
– 同步过多增加开销
• 可能的活跃度和性能问题
• 限制编译器、JVM、CPU自身的优化技术,如缓存、
重排序等
– 同步过少不能保护足够状态
• 复合操作
活跃度与性能
• 弱并发,poor concurrency– 限制并发数的往往不是可用CPU数,而是程序自身
• 如非必要不要同时使用多种同步机制防止混淆
• 锁粒度
– 耗时的操作不建议用锁,比如IO
– 缩小锁粒度,减少同步执行时间
– 又不能太琐碎,避免额外获取释放锁的开销
• 并发编程首要保证安全性,再平衡简单性与性能,不建议过早为了性能牺牲简单性
可见性与对象发布
• 可见性– 所有线程都能看到共享变量的最新值,而不是
• 过期数据,指令重排和JMM的栈变量都可能导致
• 脏数据,如非原子的64位操作
– 普通的数据读取类似于DB的READ_UNCOMMITTED隔离级别
• 发布,一个对象可以在当前作用范围以外使用– 要注意间接被发布的对象及其引用链中的对象
– 一个对象在尚未准备好前被发布叫逸出,千万不要让this引用在构造期间显示或隐式逸出
– 发布内部状态(破坏封装性),或者逸出都有线程安全风险
this引用隐式逸出
• EventListener
– 见list 3.7和list 3.8(私有构造函数和工厂方法)
• 在构造函数中启动线程
– 新线程会隐式得到未正确构建的this对象
– 可以创建但是不要启动,另外发布启动的方法
• 在构造函数中调用了可覆写的方法
– 另外发布初始化的方法
不变性
• 不变对象– 状态不能在创建后再修改
– 所有域都是final,因为final的初始化安全性保证• String延迟计算的哈希值是个例外
– 被正确创建(没有this引用逸出)– 可以仸意发布,任意共享
• 有效不可变对象– 技术上可变,但实际上状态不会变,如Date– 只需要安全发布,可以任意共享
• 可变对象– 必须安全地发布和共享
安全发布技术
• 基本原理
– 驾驭JMM的happens-before规则保证发布对象happens-before消费线程加载已发布对象的引用
• 静态初始化
• final域的原语类型
– 建议把所有域都声明为final,除非是可变的
• 不变对象
• Volatile或AtomicReference
• 隐式的锁,利用线程安全的容器类
• 其它handoff机制,如Future和Exchanger
volatile
• 弱同步,只保证原语类型和对象引用的可见性
• 不能保证操作的原子性或互斥性
• 编译和运行时都会监视volatile变量
• 不会被指令重排序• 不会缓存在寄存器或其它对CPU隐藏的地方
• 读取时总会返回某个线程写入的最新值
• 写入开销只比普通变量稍高
• 但是不能过度依赖它保证可见性,当– 能简化实现,简化同步策略时使用
– 必须确保引用状态的可用性时可以使用
– 标识重要的生命周期(如初始化或关闭)
– 还需要保证线程安全情况时,应避免使用
安全共享技术
• 线程限制技术
• 共享只读(不变和有效不变对象)
• 线程安全类(共享线程安全)
• 原子操作– Atomic类,非阻塞的原子操作
– Volatile发布不变对象,见list 3.12和3.13
• 锁(狭义的同步),保证互斥、原子、可见性– 内部锁:synchronized,wait和notify– 显示锁:java.concurrent.Lock– 文件锁:FileLock,基于操作系统可作用于进程间
锁的问题
• 摘自wiki– cause blocking
– adds overhead for each access to a resource
– vulnerable to failures and faults
– limits scalability and adds complexity
– hard to balances between lock overhead and contention
– only composable with relatively elaborate (overhead) software support and perfect adherence by applications programming to rigorous conventions
– priority inversion
– convoying
– hard to debug
– need sufficient resources for the locking mechanisms
线程限制技术
• Ad-hoc线程限制,完全依靠使用者
– 很弱不推荐,特例是volatile配吅单个写线程
• 单线程化,如采用事件分发线程的Swing或必须同步访问的JDBC以及本地库
– 利用Future+newSingleThreadExecutor+Proxy可以简化单线程化工作
• 栈(局部)变量限制
– 不要让局部对象的引用逸出,并用文档说明
• ThreadLocal
– 可以看做Map<Thread, T>,但是实现完全不同,线程结束后变量会被垃圾回收
– 用法
• 单线程迁移到多线程时,包装共享的全局变量
• 如list 3.10为每个线程分配不同的JDBC连接
• 临时缓存特别频繁或者分配开销大的对象
• 应用程序的框架上下文,例如EJB的事务
– 副作用
• 把封装的属性扩大成全局变量,会引入隐晦的类耦合,并且降低重用性
• 隐蔽了方法的参数
Java内部锁(monitor lock)
• synchronized,或wait¬ify
• reentrancy,可重进入
– 锁是per-thread粒度,而不是per-invocation
– pthread默认是不可重入的
– JVM用请求计数实现,0->1(得到)->n->1->0(释放)
• mutex,互斥锁
线程安全类设计原则
• 三要素:状态,不变约束,同步策略(需文档化)
• 原子性和封装性保证不变约束(合法状态,或有效值)和后验条件(合法状态转换)约束;状态依赖,基于状态的先验条件的操作
• 常见手段– 锁+实例限制(封装数据和数据访问)
– 委托(Delegate)线程安全
– 重用线程安全的类(并发积木)
– 组合
• 文档化同步策略
锁+实例限制
• Java监视器模式,使用私有锁而不是内部锁,例– Collections.synchronizedList(Decorator模式)– Vector
• 不能发布状态变量,需要时可以返回内部数据的deep copy– deep copy,深度拷贝,拷贝对象及其内部对象的数据而不是引用
• 简单,但是不一定能满足功能和性能需求
委托线程安全
• 可以shadow copy,返回flee view(瞬时视图)
• 适用于一个或多个独立的状态变量– 如果有多个相关的状态变量,又不能委托复合操作线程安全时,还是需要锁
• 可以发布状态变量,当且仅当– 它是线程安全的
– 没有限制它有效值的不变约束
– 没有限制它状态转换的后验条件
• 私有构造函数捕获模式,private constructor capture idiom– 避免构造函数的竞争条件?P69
重用线程安全的类
• 修改源代码– 最安全,因为同步策略都在一份文件中
– 需要理解并且不能破坏父类的同步策略
– 不不是所有类都能拿到源码
• 继承– 父类不一定暴露了足够的状态变量
– 需要理解并且不能破坏父类的同步策略
– 破坏了同步策略的封装性,因为被分散存放在不同文件
– 不适用于Collections.synchronizedList封装的ArrayList(因为不知道返回的List的实现类型)
• 客户端加锁– 扩展功能而不是扩展类本身,创建工具类
– 如果被扩展类使用锁,工具类需要使用同一把锁
– 可以扩展Vetor,Collections.synchronizedList– 破坏了同步策略的封装性
– 安全性比继承更脆弱,why?P73
• 组合– 比继承和客户端加锁更健壮,如ImprovedList不关心底层实现是否安全?同步策略是否改变?
– 增加新的锁层,需要考虑额外的性能开销,如ImprovedList只会带来微弱的性能损失
文档化同步策略
• 最应该却最未被充分利用,如– Servlet,JDBC,SimpleDateFormat都没说明是否线程安全
• 为类用户编写线程安全性担保的文档– 是否线程安全?回调是否持有锁?有没有影响行为的特定锁…
• 为类维护者编写类的同步策略文档– 哪些volatile?哪个锁保护哪些变量?哪些变量不可变?哪些变量
限制在线程中?哪些操作必须原子?扩展时应该获得的锁?有没有精巧的同步机制…
• 工具:JavaDoc,@GuardedBy
• 需要猜测类的线程安全性时,要站在实现者角度
• Servlet复制或钝化的目的,钝化是什么意思?见P76
并发积木
• 同步容器 ~1.2
• 并发容器 1.5~
• Synchronizer
– 阻塞队列
– 双端队列
– Latch
– FutureTask
– Semaphore
– Barrier
– 例子:Memorizer
并发积木-同步容器
• 分类– Vector和HashTable (~JDK 1.2)– Collections.synchronizedXXX(JDK 1.2)
• 分析– 以并发性为代价,串行执行对同步容器的访问
– 复合操作(iterator, navigation,条件运算)时,需要额外的客户端锁,否则只是技术上线程安全,不破坏类的不变约束而已,与调用者预期不一样
– 迭代和ConcorrentModificationException(ConModExcp)• 检查modification count发现并发的话及时失败fail-fast• 为了性能,检查操作并没有同步,但有过期数据风险
• 为了避免此异常,可以对迭代加锁;为了性能,也可以先复制再迭代,但是复制时也需要加锁
• 隐藏的迭代– toString, equals, hashCode,containsAll, removeAll, retainAll等
• 状态和保护它的同步之间越远,越容易忘记访问状态时正确使用同步;正如封装对象状态可以更容易保持不变约束一样,封装它的同步则可以迫使它符吅同步策略
并发积木-并发容器
• JDK 5开始引入,为并发而设计
• 同步Map的替代品– ConcurrentMap和ConcurrentHashMap
• 同步List和Set的替代品– CopyOnWriteArrayList和Set
• 新容器Queue– 非阻塞:ConcurrentLinkedQueue, PriorityQueue– 阻塞:BlockingQueue
• JDK 6– ConcurrentSkipListMap替代SortedMap– ConcurrentSkipListSett替代SortedSet
Concurrent(Map|HashMap)
• 特点– 分离锁,高并发,支持读、读写和有限写并发
– 不会抛出ConModExcp的弱一致性iterator• 弱一致性与fail-fast相对,允许并发修改,可以但不保证感应到迭代器创建后的修改
– 支持复合操作,如put-if-absent, remove-if-equal, replace-if-equal
• 注意– size和isEqual操作返回近似值而非精确值
– 不能独占Map的访问
– 不能替代同步Map实现的同步化边界效应?
– 不能使用客户端锁创建新的复合操作
CopyOnWrite(ArrayList|Set)
• 特点– copy-on-write,修改时创建元素的shadow copy– backing array,基础数组,保存元素的引用
• 作为迭代器的起点,永不会被改变
• 对它的同步只是为了确保数组内容的可见性
• 每次修改会有复制backing array的开销
– 支持并发的迭代• 不会抛出ConModExcp• 返回元素与创建迭代器时严格一致,不考虑后续修改
• 注意– 容器越大,复制的开销越大
– 适用于迭代远多于修改的情况,如事件通知系统
并发积木-Synchronizer
• 根据自身状态调节线程的控制流的对象– 控制流:通过、阻塞与中断
• 阻塞– 原因:等待IO操作,等待锁,等待Thread.sleep唤醒,等待另一个
线程的计算结果
• 中断– 允许提前结束阻塞状态,
– 但是协作机制,并不强迫线程停止正在做的事情– Thread的interrupt方法和中断状态变量
– InterruptedException,可中断的方法,捕获异常:• 传递,直接或清理后向上层抛出该异常,退出执行
• 保存中断,调Thread.interrupt保持中断,继续执行,让高层可以处理
• 不能捕获后却不做任何响应
并发积木-Synchronizer
• 分类
– JDK:阻塞队列BlockingQueue,双端队列Deque,闭锁Latch,FutureTask,信号量Semaphore,关卡 Barrier
– 自定义synchronizer(基于AQS)
• 特性– 封装状态,决定线程在某个点是通过还是等待
– 控制状态的方法
– 以及高效的状态等待方法
消费者-生产者模式与阻塞队列
• 分离了“识别需要完成的工作”和“执行工作”,解耦吅消费者和生产者,把任务细分,例如ThreadPool和Executor
• 友好支持多线程,如果消费者和生存者在不同层面执行,可以提高并发性,提高性能
• BlockingQueue– put与take,pull与offer,可定时
– 长度可能为0,固定值或者无限
– 简化消费者-生存者模式编码
– 有效的资源管理工具,可以提高超负荷工作健壮性
• BlockingQueue实现类– LinkedBlockingQueue– ArrayBlockingQueue– PriorityBlockingQueue,支持Comparable– SynchronousQueue,适用于消费者充足
窃取工作模式与双端队列
• 窃取工作模式– 每个消费者有自己的双端队列,当自己的任务消费完
时,窃取别人双端队列尾部的任务执行,保持忙碌
– 消费者不用竞争一个共享的任务队列,大多数时候访问自己的双端队列,减少竞争
– 比生产者-消费者模式具有更好的伸缩性
– 可以解决生产者和消费者同体的问题,如Web Crawler
• JDK 6引入Deque和BlokcingDeque
• 实现类– ArrayDeque
– LinkedBlockingDeque
Latch闭锁
• 特性
– 一旦闭锁到达最终状态,允许所有线程通过,并且闭锁状态永远不再改变
– 一次性使用,一旦进入最终状态就不能重置
– 确保特定活动直到其他活动完成后才发生
• CountDownLatch
– 允许一或多个线程等待一个事件集发生,如并发测试
– 做起跑线:初值=n
– 做终点线:初值=1
FutureTask
• 在需要结果之前就异步做计算,如Executor执行异
步任务
• 等待-运行-完成三个状态,完成是最终状态,run方法开始执行,get方法等待执行完成返回结果,
仸务只会执行一次
• 带时限的get方法可以设置任务执行的时间
• 运算线程的结果可以安全发布到需要结果的线程
• launderThrowable (P98)函数可以处理Throwable异常
Semaphore信号量
• 控制同时访问某资源或操作的数量
– 实现资源池,如数据库连接池
– 给容器限定边界,如有界缓冲、有界阻塞容器
• 二元信号量,可作为互斥并且不可重入锁
• List 5.14(P100)的finally块会被执行吗?
Barrier关卡
• 与闭锁类似,都能阻塞线程直到事件发生
• 与闭锁不同的是– 所有线程必须同时到达关卡点才能继续处理
– 闭锁等待的是事件,关卡等待的是其他线程
• CyclicBarrier– 可多次集中在关卡点(重置关卡),常用于并行递归算法
– 关卡失败,BrokenBarrierException– 构造函数可以传递关卡行为
– 示例List 5.15(P102)的CellularAutomata
• Exchanger,在关卡点交换数据(安全的发布),适用于两方活动不对称,两种典型策略– 写入缓冲填满,消费缓冲清空发生交换
– 缓冲填满或者充满超过一定时间发生交换
• 计算问题中,不涉及IO或共享数据的话,Ncpu或Ncpu+1个线程能产生最优吞吐量
Synchronizer实践-Memoizer
• 构建高效可伸缩的缓存,见书5.6 P103
• Memoizatin备忘录技术– 创建Computable包装器,记住之前计算的结果,并封装缓存步骤
• 解决了构建高速缓存可能的问题– 线程安全性
– 弱并发
– 重复计算
– 缓存污染
• 还需要自己动手解决– 缓存过期,扩展FutureTask,关联过期时间,周期扫描
– 缓存清理,清理过期的缓存,缓存替换算法
并发编程实践基础小结
• 所有并发问题都归结为如何协调访问并发状态,可变状态越少保证线程安全越容易
• 尽量将域声明为final类型,除非需要可变
• 不变对象天生是线程安全的,可以在没有锁或防御性复制下自由共享
• 封装数据使得它们更容易保持不变;封装同步使得更容易遵守同步策略
• 用锁守护每个可变变量
• 对同一不变约束的所有变量使用相同的锁
• 在允许复合操作期间持有锁
• 非同步多线程下,访问可变变量的程序有隐患
• 不要依赖于可以需要同步的小聪明
• 在设计时就考虑线程安全,或在文档中就明确说明它们是不安全的
• 文档化你的同步策略
仸务执行
• 仸务,抽象离散的工作单元– 清晰的仸务边界,每项任务只占用小部分CPU资源,一般用自然任
务边界——单独客户请求,否则需要进一步分析
– 独立的活动,不依赖于其他任务状态或边界效应,有利于并发
– 仸务是逻辑工作单元,线程是任务异步执行的机制
– 围绕任务执行构建程序,可以简化开发便于同步
• 正常负载下,应用应具备良好的吞吐量(减少成本)和快速的响应性(提高体验);过载下,应该平缓地劣化
• 并行运行异类任务有局限性– 耗时各不不同,可能需要重新分配资源,额外的开销
• 大量相互独立且同类的任务才能发挥最大并发性
仸务执行策略4w要素
• 仸务在what线程中执行
• 仸务以what顺序执行(FIFO,LIFO,优先
级)
• 可以有how many个仸务并发
• 可以有how many个仸务进入等待队列
• 如果过载应该放弃which仸务?How通知应
用程序这一切
• 在仸务执行前后应该做what处理
仸务执行策略
• 顺序执行
– 一次接受一个请求,低吞吐量,高响应时间
– GUI程序是特例,一般都是顺序执行
• 每任务每线程thread-per-task– 并发执行,中等负载(qps不会超过tps)下勉强可用
– 创建关闭线程的额外开销,高资源消耗(如内存),低稳定性
• 线程池执行
– 工作队列,持有所有等待执行的任务
– 重用线程,提高响应性,提高利用率,防止过多资源竞争,提高稳定性
• Java的Executor框架– 可以实现上述执行策略
– 简化任务和线程的生命周期管理
– 解耦吅仸务的提交和执行策略
Executor框架-Executor
• Executor– 线程安全
• 提交线程到执行线程安全发布,执行线程到获取结果线程安全发布
– 基于生产者-消费者模式,解任务提交(任务内容)和任务执行(策略)的耦吅
– 提供了生命周期和钩子函数支持,便于扩展
– 使用Runnable作为其任务的基本表达式,但是Callable更适吅• Callable能更好支持耗时的复杂任务,并为可能抛出的异常预先做了准备
• Executor仸务的生命周期:创建-提交-开始-完成,正好对应Future• 总可以取消提交但尚未开始的任务;已开始任务只有响应中断才能取消
• 用Executors静态工厂方法和ThreadPoolExecutor创建线程池
• RejectExecutionHandler拒绝执行处理器– Executor已关闭,或超过工作队列最大长度
• ScheduledThreadPoolExecutor完美替代Timer– 基于相对时间,Timer基于绝对时间对系统时钟改变很敏感
– Timer的缺陷:单线程,不能捕获运行时异常,线程泄漏(见list 6.9 P161)
– 用DelayQueue构建自定义的调度服务
Executor框架-ExecutorService
• 暗示Executor的生命周期:运行-关闭-终止
• 覆盖newTaskFor方法可以控制Future实例
化,默认实现仅创新FutureTask实例
• submit方法都会返回Future,可以在需要时
进行取消或获取结果
Executor框架-CompeletionService
• 用于处理批处理任务,像打包的Future,有一个仸务完成就能取到结果,提高响应性
• 整吅了Executor和BlockingQueue,利用FutureTask在计算完成时会调用done方法,QueueFuture覆盖此方法把计算结果加入BlockingQueue
• 见list 6.15 P130,
• 如果要限制批处理的时间,可以用ExecutorService的invokeAll方法,阻塞等待所有线程要么完成、
要么中断、要么取消才会返回
仸务取消和关闭
• 需要重视生命周期结束问题(取消和关闭)
– Java没有安全关闭线程的机制,Thread.stop和suspend方法有严重缺陷
– Java只有协作的中断机制或自行设置取消标志
• 取消:仸务取消和线程中断– FutureTask和Executor框架可以简化可取消任务的设计
• 关闭:异常导致的线程关闭和JVM关闭
仸务取消
• 可取消的,外部代码能够在活动自然结束之前把它改为完成状态– 用户取消,超时,内部事件,错误,关闭等
• 设置取消请求标志,仸务不定时检查响应取消– 一种协作机制,别人只能请求,取不取消在我自己
• 仸务取消策略包含– 其他代码how请求取消任务
– 仸务在when检查取消请求是否到达
– 响应请求取消的任务要做what
线程中断
• 中断,另一种引起线程关注的协作机制,采用signal实现
– 非强制,只是请求线程中断,线程在取消点自行响应
– 往往用来实现任务取消
– 还可以做别的工作,如ThreadPool的工作线程
• Java对中断的支持
– boolean型中断状态
– interrupt,请求中断
– isInterrupted,检查中断
– Thread.interrupted,清除中断,返回清除前中断状态
– 阻塞方法(sleep和wait等),清除中断,抛出InterruptedException
– 非阻塞状态下发生中断,不会抛出异常,只是设置了中断状态,等待任
务自行检查后处理(中断的粘性)
– 不能提供优先中断,还强迫开发处理中断异常,但是可以灵活地平衡程
序的响应性和健壮性
中断策略
• 收到中断请求时,线程如何响应
– 线程会做什么
• 要么取消当前工作,要么保存中断(interrupt),或者重新抛出中断异常
• 不能无理由掩盖中断,阻止其他程序失去响应中断的机会
– 哪些工作单元对中断来说是原子操作
– 应该在多快时间内响应中断
• 常见的线程级和服务级中断策略:尽可能迅速退出,如需
要进行清理,可能的话通知其拥有的实体该线程已退出
• 要区分仸务和线程的中断策略,因为执行任务的线程(如
线程池)可能不是所有者,执行线程应该把中断状态传递
给所有者,就像阻塞函数的中断策略——尽可能快的抛出中断异常
• 中断策略设计很重要,保持中断策略一致更重要
Java中断响应实践
• 可中断的(阻塞)方法,即能抛出InterruptException
– 传递,直接重新抛出
– 保存,在不应该(list 7.7 p144)或不能(如Runnable)传递时
– 通用目的的仸务和库的代码绝不应该吃掉中断请求;只有实现了中断策
略的代码才可以吃掉
• 不支持中断的非阻塞方法,通过检查当前线程的中断状态来响应中断,
需要在效率和响应性间仔细权衡
• 不可中断的阻塞方法,可以模拟中断异常
– java.io的同步socket io,关闭底层socket,抛出SocketException
– java.nio的同步io
• 中断一个等待InterruptibleChannel的线程,抛出CloseByInterruptException
• 关闭一个InterruptibleChannel导致多个阻塞线程抛出AsynchronousCloseException
– selector的异步io,调用close方法抛出ClosedSelectorException
– 等待锁,采用显示锁的lockInterruptibly方法,它可以响应中断,见第13章
Java中断响应实践
• 实例1,timedRun限时执行
– bad one(list 7.8 p145)
– simple one( list7.2 p137),sleep实现
– normal one(list 7.9 p146),join实现
– good one(list 7.10 p147),Future实现
• 实例2,封装非标准的中断取消
– ReaderThread(list 7.11 p149),重写interrupt方法增加关闭socket的调用
– SocketUsingTask&CancellingExecutor(list 7.12 p151),重写ThreadPool的newTaskFor钩子函数创建自定义的Future,自定义的
Future重写cancel增加关闭socket的调用
利用中断实现优雅的关闭
• 一般地,线程的所有权属于创建它的类
– 线程的所有权不能被传递
– 比如,app拥有service,service拥有worker,但app不拥有worker,所以app不应该直接停止worker,service应该提供生命周期方法供
app调用(如ExecutorService的shutdown和shutdownNow)
• 优雅关闭,与强制关闭相对,它可能允许等待正在或即将
运行的仸务执行完毕,或清理io,或解除被阻塞线程等等
– 见list 7.13-16 p153-156的异步日志服务
– 要解决可能的竞争条件
– 取消一个生产者-消费者任务既要取消消费者也要取消生产者
• 利用ExecutorService代理服务的生命周期方法
• 致命药丸模式,适用于消费者和生产者数量已知并且数量较少,和无限队列
ExecutorService的关闭方法
• 优雅的shutdown
– 安全但是响应慢
• 强制的shutdownNow
– 响应快但不安全
– 返回结果只包括尚未开始的任务,要得到正在执行的
仸务,可参考TrackingExecutor(见list 7.21 P159)
• shutdown和shutdownNow
– 幂等的 ,对已关闭的线程池无影响
– 异步的,与awaitTermination一起用阻塞等待关闭完成
异常导致的关闭
• 未被捕获RuntimeException可能导致线程泄漏,如
Timer代表的服务( P124 )永远得不到执行
• 通过Runnable等abstraction调用未知、不可信代码
时要考虑捕获RuntimeException,否则个别程序的
问题可能影响到整个应用,或者导致线程泄漏– 主动方式,如ThreadPool的工作者线程,或Swing的事件分发机制,
或动态载入的插件
– 回调方式,利用jvm的UncaughtExceptionHandler
– 两种方式要互为补充才能够避免线程泄漏
Thread的UncaughtExceptionHandler
• 当线程因为未捕获异常退出时,jvm会自底向上把这个事
件报告给一个能处理的handler– 默认handler是向System.err打印线程堆栈
– Jdk1.5之前,继承ThreadGroup修改handler
– Jdk1.5开始,通过Thread.set[Default]UncaughtExceptionHandler设置
• JVM自底向上查找handler,找到一个就行
– Thread->ThreadGroup->Parent ThreadGroup->…->Top ThreadGroup->Custom Default Handler(默认空)-> JVM Default Handler(print to System.err)
• 至少要将异常记录到应用程序的日志中
• 可以通过ThreadFactory给线程池设置handler
– execute提交的线程采用此方法
– submit提交的线程会把异常包装成ExecutionException,作为任务返回状态
的一部分通过Furture.get重新抛出
JVM关闭的类型
• 正常关闭,如最后一个非daemon线程结束,或
System.exit,或OS操作(SIGINT#2或Ctrl+C)
– JVM不会尝试停止运行中的应用线程
– JVM会立即启动所有shutdown hook关闭钩子
– 所有关闭钩子都执行完成后,JVM还可能执行finalizer
– 应用线程在JVM最终停止时被强制退出
– 关闭钩子和finalizer可能延迟JVM关闭甚至挂起(死锁)
• 强制关闭,如Runtime.halt,或杀死JVM对应的os进程
(SIGKILL#9)– 立即停止,不会启动shutdown hook
JVM关闭细节
• 关闭钩子,通过Runtime.addShutdownHook注册,JVM正常关闭时启动
– 可用于应用程序清理,比如删除临时文件
– 并发乱序执行,可能与其它关闭钩子或应用线程同时执行,所以必须是线程安全
– 关闭钩子不应该依赖于应用程序或者其他钩子关闭的服务,否则可采用
唯一的关闭钩子方式
• Daemon线程,与普通线程只在JVM关闭时又区别
– JVM启动时除了main都是Daemon线程
– 新线程继承创建者的Daemon属性
– JVM停止时不会执行daemon线程的finally块,也不会释放栈
– 用于执行IO操作很危险,最好用于housekeeping仸务
• Finalizer,让jvm在做gc时显式清理本地资源,如文件或socket句柄等– 可运行在JVM管理线程中,访问任何对象,因此必须被同步
– Finalizer运行时没有保证,编写复杂,还可能带来巨大性能开销,尽量避免使用
– 不如用finnaly结合显式close方式来清理本地资源
线程池应用
• 虽然Executor框架可以解耦仸务提交和执行,但是并非所
有仸务都适合所有的执行策略,有些仸务需要明确指定执
行策略,这就是任务和执行策略的隐性耦合,这些类型包
括:
– 依赖性,不是独立的,依赖时序或其他任务结束或边界效应的任
务,引起线程饥饿死锁等活跃度问题,需要线程池无限大
– 线程限制,需要Executor保证单线程化(newSingleThreadExecutor)
– 响应性敏感,Executor无法保证响应时间,如GUI或存在耗时任务
– 使用ThreadLocal,因为Executor会重用线程,可能导致线程不安全
• 同类型独立的任务线程池还能有最佳工作表现,
否则可能导致线程安全或活跃度问题,对于特定执行策略和配置要记录下来防止被破坏
定制线程池大小(工作队列大
小)• 切记硬编码,至少可配置,动态计算更好
• 依据:计算环境(CPU),资源预算(内
存、文件、socket、JDBC连接、资源池),
仸务特性(计算密集?io密集?混吅操作?行为差别?负载差别?)
• 计算密集:Nthreads=Ncpu+1
• 阻塞操作:Nthreads=Ncpu*Ucpu*(1+W/C)
Nthreads =池大小,Ncpu =CPU数,Ucpu = CPU利用率,W/C=等待时间/计算时间
创建ThreadPoolExecutor
• Executors的工厂方法创建线程池
– newFixedThreadPool,核心池=最大池=n,永不超时,无限阻塞队列
– newCachedThreadPool,核心池=0,最大池=Integer.MAX_VALUE,超时=1min
– newSingleThreadExecutor,核心池=最大池=1,无限阻塞队列
– newScheduledThreadPool
– newSingleThreadScheduledExecutor
• new ThreadPoolExecutor()– int corePoolSize,核心池大小
– int maximumPoolSize,最大池大小
– long keepAliveTime,存活时间
– TimeUnit unit,存活时间单位
– BlockingQueue<Runnable> workQueue,工作队列
– ThreadFactory threadFactory,线程工厂
– RejectedExecutionHandler handler,饱和策略
ThredPoolExecutor配置之线程生命周期
• Executor&ThreadPoolExecutor&Executors
• ThredPoolExecutor的构造参数– corePoolSize核心池大小,也是线程池试图维护的目标大小,工作
队列充满前不会创建新线程
– maximumPoolSize最大池大小,线程并发上限
– keepAliveTime&unit,非核心线程闲置时间上限
• ThredPoolExecutor的方法– prestartCoreThread&prestartAllCoreThreads,仸务提交之前就启动
核心线程,使他们idly for work
– allowCoreThreadTimeOut,允许核心线程超时销毁
ThredPoolExecutor配置之工作队列
• 工作队列就是等待队列或阻塞队列,即构造参数的workQueue
• 类似网络通信的流量控制,保护线程池妥当执行,又能缓冲一定的突发任务
• 三种排队行为
– 无限队列
• 无限LinkedBlockingQueue(LBQ),如newSingleThreadExecutor和newFixedThreadPool
– 有限队列
• ArrayBlockingQueue(ABQ), 有限LBQ,PriorityBlockingQueue(PBQ)
• 队列长度要和池大小要一起调节
• 饱和策略
– 同步移交syncronous handoff• 等待队列长度为0,SynchronousQueue(SQ),如newCachedThreadPoool
• 实践经验
– LBQ和ABQ是FIFO,要控制仸务执行顺序的话用PBQ
– SQ比LBQ的等待性能更好,尤其是java 6中SQ采用了新非阻塞式算法
ThredPoolExecutor配置之饱和策略
• 有限队列充满,或任务提交到已关闭的Executor时,触发
饱和策略
• 即构造参数的rejectedExecutionHanlder,java提供:– AbortPolicy(默认),抛出RejectedExecutionException
– DiscardPolicy,丢弃新任务
– DiscardOldestPolicy,丢弃接下来执行的任务,再重新提交新任务
(如果采用优先级队列那丢弃的将是最高优先级的任务,所以别
同时使用它们)
– CallerRunsPolicy,调用者运行,转移过载负荷,如http服务器中,
负荷从线程池->工作队列->应用->TCP->User逐层转移
– Java没提供阻塞execute方法的策略,可以用Semaphore模拟,见list 8.4 P176
ThredPoolExecutor配置之线程工厂
• 即构造参数的threadFactory
• 线程池通过ThreadFactory.newThread创建线程
– 默认:新的、非daemon线程
– Executors#privilegedThreadFactory安全策略,新线程继
承创建该工厂的线程的权限和上下文,否则是创建新线程所需要的权限,会引起安全异常
– 自定义线程工厂用途
• 配置UncaughtExceptionHandler
• 更有意义的名称用于日志或dump,见list 8.6 P177
• 定制Thread,如执行调试日志、统计等,见list 8.7 P178
• 修改线程池优先级,不推荐
• 修改线程的daemon性,不推荐
可调试性
ThreadPoolExecutor实践
• newCachedThreadPoool拥有比newFixedThreadPool更好的等待性能
• newFixedThreadPool可以限制并发数量,比如http服务器
• 有限线程池或有限等待队列适用于独立任务,否则要采用
无限线程池,或者有限池+SynchronousQueue+调用者执行
• 直接用构造函数创建的ThreadPoolExecutor,创建后可以用
setters修改参数
• 除newSingleThreadExecutor外,Executors工厂方法返回的ExecutorService也可以用setter修改参数,见list 8.8 P179
• newSingleThreadExecutor是用unconfigurableExecutorService方法重新包装过的,禁止修改参数避免破坏其语义,我们
也可以用这个方法来暴露不可修改的ExecutorService
ThreadPoolExecutor扩展
• 可扩展的钩子,扩展线程池行为
– beforeExecute,仸务执行前
– afterExecutor,仸务执行后
• 无论任务正常结束,还是抛出Exception都会被执行
• 但是beforeExecute抛出异常,或仸务抛出Error不会被执行
– terminate,线程池关闭后
• 可用于统计、调试、释放资源等,见list 8.9 P180
并行递归算法
• 循环并行化,需要每个迭代彼此独立
• 比如树遍历,每个结点计算是独立的,可以顺序遍历并行
计算,然后用shutdown和awaitTermination等待遍历结果,
见list 8.11&8.12 P182
• 再比如谜题解决框架,见list 8.13~18 P183~188
• 并行程序不像顺序程序,顺序搜索完后自然就终止了,并
行程序可能因为无解而无法终止,需要增加额外的终止条
件– 仸务计数器,为0时候就终止,见list 8.18
– 时间限制
– 跟puzzle相关,比如只搜索确定数量的结点
– 暴露给用户
GUI
• “多线程GUI Toolkit is another failed dreams in CS”,如失败
的AWT,因为锁顺序不一致容易诱发死锁,如
– 输入事件处理和GUI组件更新之间,前者从os->gui->app,后者从app->gui->os,完全相反
– MVC中,可能C调用M,M再通知V,也可能C调用V,V再回调M
• GUI悖论
– 所以,几乎所有GUI都是单线程,比如Swing的事件派发线程EDT,Swing数据类型同样也是为单线程设计的
– 但又不应该在事件线程中执行耗时操作以免UI失去响应
• 单线程GUI
– 所有GUI对象(可视化组件和数据)都只能被事件线程访问
– 把线程安全的一部分工具推给应用程序开发者
– 顺序事件处理,事件线程中执行任务必须尽快结束返回事件线程
Swing的线程限制
• 单线程规则:Swing的组件和数据模型只能在EDT中被创建、
修改和请求,所以这些对象不用同步仅靠线程限制来保持
一致性
• 特例,如非特别说明均可被任意线程调用
– SwingUtilities.isEventDispatchThread,判断当前线程是否EDT
– SwingUtilities.invokeLater,安排Runnable在EDT中执行
– SwingUtilities.invokeAndWait,安排Runnable在EDT中执行并阻塞当
前线程,仅用于非GUI线程
– 将repaint或revalidation请求插入队列的方法
– 添加或移除Listener的方法,但是Listener一定要在EDT中执行
• GUI开发有用的例子
– list 9.1 P193 用newSingleThreadExecutor实现SwingUtilities很简洁
– list 9.2 P194 GuiExecutor把仸务执行都通过SwingUtilities委托给EDT
GUI编程实践
• 短期仸务,全部动作都可以在EDT中执行
• 耗时任务,采用list 9.4-8 P196-200的BackgroundTask等例子,或SwingWorker类– 后台执行,取消,进度提示,完成通知
• 共享数据模型,比如文件、db、远程服务
– 线程安全的数据模型
• 用invokeLater把异步获得数据push到EDT
• EDT轮询poll数据是否存在
• 版本化数据模型,如CopyOnWriteArrayList
– 分拆数据模型,分拆为表现和应用模型
• 表现模型已被EDT限制了,后者才是真正被共享的数据模型,即共享模型
• 表现模型添加对应用模型更新的Listener,让应用模型更新时通知到表现模型
• 表现模型可以直接从应用模型获取数据,或后者给前者发送快照或增量更新
避免活跃度危险
• 安全性和活跃度通常是互相牵制的
• 死锁是最主要的活跃度问题,还有活锁等
• 回忆下经典死锁“哲学家吃饭”问题
• 再回忆死锁的四个必要条件,即Coffman conditions
– Mutual exclusion互斥,资源只能同时被一个线程占有
– Hold and wait请求并保持,处理已占有资源时还会请求新资源
– No preemption非抢占式,资源只能被占有它的线程释放
– Circular wait循环等待,多个线程循环请求下一个占有的资源
• 死锁的危害
– db可以检测以及从死锁中恢复(强制回滚一个事务?)
– java遇到死锁就game over,只能重启
– 而且死锁往往发生在高负载下,后果非常严重
死锁的类型
• 锁顺序死锁,见list 10.1 p207
– 一般来说是两个方法请求锁顺序不一致,较易发现,采用一个顺序即可
• 动态锁顺序死锁,见list 10.2 p208
– 需要分析特定运行时下才能发现
– list 10.3 p209采用加时赛锁解决顺序相等情况
• 协作对象间死锁,见list 10.5 p212
– 在持有锁的情况下调用外部方法,外部方法可能再请求锁或者遭遇严重
阻塞,较难分析
– list 10.6 p214用开放调用减小锁粒度避免死锁
– 开放调用,不需要 拥有锁就能调用的方法,即同步方法
• 盲目使用同步方法肯定要不得
• 但是开放调用可能丧失原子性,要仔细评估其实际上、语义上的影响,或寻求并发对象
• 资源死锁,请求锁的不是对象,而是cpu以及db等资源– 如db,两个方法按照不同顺序更新多个记录或请求多个数据源
– 如cpu,有依赖关系的任务在一个有界线程池中执行,池越小依赖越多发生死锁频率就越高
死锁检查和避免
• 原则:至少破坏一个造成死锁的必要条件
• 基本方法:
– 如果程序至多只会请求一个锁最好,但不现实
– 仔细设计锁顺序,首先识别什么地方会获取多个锁,再全局分析保证锁顺序一致
– 尽可能采用开放调用,能够简化core review工作,甚至可以用自动化工具
分析源码或字节码
• 定时锁,即Lock.tryLock超时方法版本
• thread dump分析
– 触发:Unix下发生SIGQUIT#3或ctrl-\,Windows下ctrl-break
– dump前jvm会进行死锁分析,发现死锁话会包含在dump信息中
– jdk 5不支持Lock的dump,jdk6支持但是信息比内部锁少(内部锁会打印获
得它的线程栈框架,Lock只会打印获得它的线程)
其他活跃度问题
• 饥饿,线程访问它所需要的资源却被永久拒绝
– CPU周期是最常见引发饥饿的资源,比如错误使用优先级api,或者获得锁
后做无限循环或等待
– 尽量避免使用优先级api,因为会增加平台依赖性并可能引起饥饿
• 弱响应性
– cpu密集的后台仸务,可以降低它们的优先级
– 长时间占有锁,比如对大容器迭代并进行耗时操作,要避免
• 活锁,线程虽然没被阻塞但是不断重试不断失败
– 如现实生活中,两个过于礼貌的人相遇,同时避让再避让
– 如消息处理程序中,把处理失败的消息放回队首接着处理
– 如多线程协作中,为了响应而修改状态导致彼此都不能继续
– 重试引入随机性,如以太网的冲突检测避免
• 丢失信号(来自chap14.2.3)– 等待已经发生过的事件给它通知,甚至有可能永远不会有通知,永远等待
– 如果在调用wait之前先检查条件谓词就能避免该问题,见list 14.7 p301
性能和可伸缩性
• 提高性能的目的是提高– 资源利用率,资源包括CPU、MEM、Bandwidth、IO、db、disk等
– 系统响应性
• 避免不成熟的性能优化
– 在设计并发程序初期就考虑性能,往往不是最重要的
– 首先要保证正确性(安全性等),然后才在必要时进行优化
• 多线程引入的开销
– 协调开销,如加锁、信号量、内存同步
– 上下文切换,如os线程在用户态和内核态切换
– 线程生命周期管理,如线程创建和销毁
– 线程调度,如线程池管理
衡量性能指标
• 有多快(给定仸务的处理时间)
– 如服务时间,等待时间,效率
– 为效率而进行的调优,其目的通常是用最小的代价完成相同的工作,比如缓存,
改进算法时间复杂度
• 有多少(给定资源能够完成的任务数)– 如生产量,吞吐率,可伸缩性(增加计算资源时,吞吐量和生产量能够相应改
进)
– 为可伸缩性进行调优时,目的是如何并行化问题,使得能够利用额外的计算资
源,用更多的资源做更多的事情
• “有多快”和“有多少”两方面是完全分离的,甚至相悖– 大多数单线程化程序提高性能的窍门都会损害可伸缩性
– 为了实现更好的可伸缩性或更好利用硬件,我们可能会分解任务、分层引入额外
开销,降低单线程性能,如MVC模型
– 但是单一系统有处理极限问题,提升性能会越来越难;我们可以接受稍长的时间
开销换取可伸缩性,如server程序;交互式应用更关心等待时间
评估为性能优化而做的权衡
• 权衡,即折中,为了现实解决问题而做出的工程决定
– 用某种成本换取其他东西,如用空间或复杂性或封装换时间
– 用开销换安全性,如为了线程安全增加同步
• 如何判断某个方案性能“更快”
– 更快指的是什么?
– 在什么样条件下你的方案能够真正运行得更快?在轻负载还是高负载?
大数据集还是小数据集?是否支持你的测量标志的答案?
– 这些条件在你环境下发生的频率?是否支持你的测量标准的答案?
– 这些代码在其他环境的不同条件被用到的可能性?
– 牺牲了什么隐含代价(如增加开发风险或维护性),换取性能提高?这
个权衡是否合理?
• 不要拍脑袋随意臆测,要测评
– 根据性能需求和评估纲要,使用现实的配置和负载状况来进行性能调节,调节过后还要再评估,以验证期望的改进
Amdahl定律介绍• 所有并发程序都是由一些并行和串行片段构成的
– 如果你认为没有串行片段,请仔细检查
• wiki/百度百科,并行程序的可伸缩性受限于串行化比重
• 量化分析并行程序的可伸缩性
– 见后两页ppt,F:串行比重,N:cpu数
• 我们需要识别逻辑边界,把程序分解为不同任务,从而提高程序并行
性;同样地,为了在多CPU系统中预知程序是否存在加速可的能性,
还需要识别任务中串行部分
• 定性使用Amdahl定律
– 评估算法时,考虑其在成百上千个处理器情况下受到的限制能够帮助我
们洞察其可伸缩性的极限
– 如定性分析分拆锁和分离锁,后者可伸缩性比前者好,因为分离锁的数
量可以随着处理器的增加而增加
Amdahl定律1
• 并行系统的最大性能提升受限于程序串行比重
• 加速比:speedup<=1/(F+(1-F)/N)
识别任务中串行部分
• 共享的数据结构,如阻塞队列
• 结果处理,必须有否则就是死代码,如日
志或放回共享数据结构
• 框架中隐藏的串行化,如图11.2 p228
线程引入的开销-上下文切换
• 上下文切换,保存当前线程上下文,并重建新调入线程的上下文
– 每次需要5k~10k时钟周期,约几微妙
– 切换时需要操控os和jvm的共享数据结构
– 本地没有新调入线程所需的数据引起缓存缺失
• 原因
– 运行线程数超过了CPU数量,os会按照时间片轮流分配CPU周期
– 阻塞越频繁上下文切换次数越多(阻塞时,jvm通常会将该线程挂起,允
许它被换出,线程就不能完整使用它的时间片)
• 分析– unix的vmstat和mpstat,windows的perfmon工具
– 高内核占用率(10%)象征繁重的调度活动
• 办法
– 非阻塞式算法:减少锁竞争,减少阻塞,减少上下文次数
线程引入的开销-内存同步
• memory barrier存储关卡
– synchronized和volatile的可见性需要用存储关卡指令来刷新缓存、使缓存无效、刷
新硬件写缓冲、并延迟传递执行
– 抑制编译器优化,大多数操作不会被重排序
• 非竞争的同步
– volatile总是非竞争的同步
– synchronized优化了非竞争同步,如fast-path只需要20~250CPU周期,影响微乎其微
• jvm对竞争的同步的优化
– 可以解除经确认不存在的竞争锁,见list 11.2 p230
– 运行时逸出分析,吅并锁调用,如ibm的锁省略,见list 11.3 p231
– 锁粗化,见list 11.3 p231
• 不用过分担心非竞争的同步,jvm做了足够的优化,只需要关注真正发生锁竞
争的区域
• 同步还会增加共享内存总线的通信量,所有进程都共享一条总线,带宽有限
线程引入的开销-阻塞
• 阻塞原因:锁竞争,阻塞io,条件等待等
• jvm自旋等待spin-waiting,或在os中挂起suspending被阻塞的线程,效率取决于
– 上下文切换开销和等待时间
– 自旋等待适吅短期等待,挂起适吅长时间等待
– 但是大多数等待锁的线程都是被挂起
• 挂起需要两次额外的上下文切换(换出和换
入),以及os和缓存的相关活动
减少锁竞争
• 串行化会损害可伸缩性,上下文切换会损害性能
• 竞争性锁既有串行化(访问独占锁)又有上下文切换(阻塞),会同
时导致两种损失
• 锁的竞争性
– 锁请求的频率和每次持有锁的时间(Little定律推论)
– 如果两者乘积足够小,就是是非竞争性的
• 三种减少锁竞争的方式
– 减少持有锁的时间
• 缩小锁范围(快进快出),但是别忘了原子操作
– 减少请求锁的频率
• 减小锁粒度:采用相互独立的锁能够减少通信,如分拆锁和分离锁,但是增
加了死锁风险
• 尽量避免池化对象
– 用协调机制取代独占锁
减少请求锁的频率
• 分拆锁
– 每个锁守护相互独立的状态变量
– 垂直拆分,允许不同线程访问不同数据
– 对锁的竞争普遍大于对锁守护数据的竞争
– 适用于竞争不激烈的锁,可以改进性能和可伸缩性,如拆分中等强度的锁能够转化成非竞争的锁
• 分离锁
– 有些分拆锁还可以细分,分拆成可大可小加锁块的集合,并且它
们属于相互独立的对象;水平拆分,允许不同线程访问相同数据
的不同部分
– 副作用:整个对象独占访问的开销更大,需要一次性获得所有锁
– 如ConcurrentHashMap的16(默认)个锁
热点域和替换独占锁
• 热点域
– 每次操作都会请求的变量,会限制可伸缩性
– 需要仔细设计策略,如ConcurrentHashMap.size方法
– 尽量避免使用热点域
– 或者用原子变量实现
• 替换独占锁
– 并发容器
– ReadWriteLock,读并发写独占
– 不变对象,只读
– 原子变量,用硬件原语实现比如cas
• 避免用池化对象来优化性能,得不偿失
– 原因:池大小,没完全重置风险,old-to-young引用,额外同步,锁竞争
– 实际上从1.4开始,分配对象只需要10几个指令,比malloc快
• Map的性能比较,见图11.3 p243
监测CPU利用率
• 测试可伸缩性的目的是保持处理器充分利用
• 工具
– unix: vmstat, mpstat, iostat
– windows: perfmon
– profiler工具
• 低利用率的原因:
– 不充足的负载,需要客户端产生足够的负载使得应用程序饱和
– IO限制,硬盘,网络等等
– 外部限制,profiler帮助db、web service等外部服务
– 锁竞争,profiler或dump帮助找到锁竞争
• 判断能够通过增加cpu程序的收益,如从4-way到8-way
减少上下文切换的开销
• 再度思考服务器程序的日志服务
• 同步的日志,对println进行瘦封装
• 异步日志带来的复杂度
– 中断:阻塞在日志操作的线程被中断?
– 服务担保:保证成功加入的日志消息都被记录?
– 饥饿策略:当生产者记录消息速度超过logger处理速度
– 生命周期,如何关闭lottery,如何就服务状态与生产者沟通
• 异步日志带来的好处,见list 7.13-16 p153-156– 减少log服务等待和服务的时间,总体提高log服务响应时间,因为
实际上提交立刻就能返回,put操作比实际io开销小
– 统一的代码路径,更简单的锁策略
– 减少对输出流的竞争
测试并发程序
• 目的:不是更多地发现错误,而是提高信心
• 挑战
– 潜在错误的发生具有不确定性,是低概率事件受多种条件影响
– 额外的同步或分时约束可能屏蔽潜在的并发问题
– 需要更广泛的覆盖度和运行时间才能揭示上述问题
• 分类
– 安全性测试,“什么坏事都没发生过”
• 验证不变约束,但是如何消除验证时的竞争?
– 活跃度测试,“好的事情终究会发生”
• 包含“应该执行”和“不应该执行”两方面
• 如何量化活跃度?怎样验证线程被阻塞了?怎样确保算法不会死锁?
• 性能测试,吞吐量、响应性和可伸缩性
• JSR166专家组的测试基类:JSR166TestCase.java
测试正确性
• 基本的单元测试:并发测试都应该包含基本的顺序测试
• 测试阻塞操作:类似于测试异常,但是方法被阻塞后还必须解除阻塞
– 可以利用中断异常,需要准确估计等待线程阻塞的时间,见list 12.3 p251
– Thread.getState不可靠,只能做调试信息
• 测试安全性
– 挑战:简单识别不安全的属性,避免测试程序限制了被测代码的并行性
– 顺序敏感和顺序不敏感(如可交换的加、异或操作)的checksum函数
– 避免恒定的测试数据,用RNG或xorShift函数(list 12.4 p153)
– 避免领先优势,用两个CountDownLatch(1和n)或CyclicBarrier(n+1)控制并发测
试开始和结束,见list 12.5-6 p154-5
• 测试资源管理
– 属于不应该做的事情,比如资源泄漏,见list 12.7 p158
• 使用回调
– 如Executor的ThreadFactory,见list 12.8-9 p258-9
• 产生更多的交替操作
– 线程数大于cpu数,和Thread.yield,见list 12.10 p260
测试性能
• 包含一些基本的功能测试是值得的
• 目标
– 探寻具有代表意义uc从头到尾的性能
– 为线程数、缓存容量等选择合适的值
• 示例:list 12.11- p261-
• 测量吞吐量,总时间/总数量(平均响应时间)
– 利用Latch和Barrier的动作简化开始停止时间的记录
– 测试多种参数的组合得到更准确的结果
– 多种算法比较,如LBQ的伸缩性比ABQ更好,因为LBQ允许队列头尾同时更新
• 测量响应性,每个独立动作的耗时(服务时间差异性)
– 可预言性:为了获得较小的服务时间差异允许较长的平均服务时间是有意义的
– 测试差异性帮助我们了解QoS服务质量,柱状图呈现
– 如非公平信号量提供更好的吞吐量,公平信号量提供更低的差异性
避免性能测试陷阱
• 减少垃圾回收影响– -verbose:gc选项,打印gc信息
– 或者确保多次执行gc,该策略更佳因为需要更长的测试时间更接近真实情景
• 减少动态编译影响
– 首次加载类以解释方式执行,如果一个方法执行足够频繁会被动态编译器转化成本地代码,编
译完成后从解释改为编译执行;代码也可能被解除编译以及再次编译,比如加载新类或收集足
够统计后决定做新的优化;解释执行、编译执行和混合执行的性能差别很大
– 要么长时间执行(至少几分钟),使编译和解释执行只占总时间的小部分
– 或者暖身:需要所有代码编译完成后,才开始真正的测试
– 用hotspot –XX:+PrintCompilation打印动态编译信息
– 运行多个不相关的CPU密集仸务之间最好显示暂停,使得任务和jvm后台得以同步
• 避免代码路径的非真实取样
– 尽量接近典型应用场景
– 尽量覆盖应用会用到的代码路径的集合
• 避免不切实际的竞争程度– 除了关注并发协调,还要尽量模拟真实的协调以外的工作内容,以接近真实的竞争程度
• 避免测试代码被编译器优化成死代码
– 使用每个结果(避免实际io),如if(foo.x.hashCode() == System.nanoTime()) System.out.print(“”)
– 而且结果应该是不能被预测的,不要使用静态数据作为输入
其它测试方法
• 代码审查
– 与单元和压力测试同等重要,但不能取代前者
• 静态分析工具
– 如FindBugs(见p271-2):不一致同步策略、Thread.run、未释放的锁;
空synchronized块、双检查锁、构造函数启动线程、notify错误、条件等待
错误、无用Lock和Condition、休眠等待时持有锁、自旋循环
• 面向方面的测试
– 暂时比较薄弱,因为不支持同步点的pointcut
– 用来断言不变约束或遵循同步策略一方面还是不错的,例如断言所有对
非线程安全的Swing方法的调用都发生在事件线程中
• 统计与剖析工具
– 商业工具
– JMX代理提供有限的线程监控,见ThreadInfo类
显示锁
• Lock的实现必须与内部锁具有相同的内存可见性
语义,但是加锁语义、调度算法、顺序保证、性
能等可以不一样
• public interface Lock
– void lock()
– void lockInterruptibly throws InterruptedException
– boolean tryLock()
– boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
– unlock()
– Condition newCondition()
ReentrantLock特性
• 可轮询:tryLock(),可用于避免死锁
– 如list 13.3 p280 即避免死锁又能优雅失败
• 可定时:tryLock(long, TimeUnit)
– 可用于有时间限制的活动,见list 13.4 p281
• 可中断,lockInterruptibly和tryLock(long, TimeUnit)
– 可用于允许取消的活动,见list 13.5 p281
• 非块结构,加锁和释放不在同一个程序块
– 如ConcurrentHashMap遍历操作需要的链式加锁(或锁联接)
– 可能导致thread dump和jvm优化很难支持
• 性能– jdk5中显示锁的竞争时性能比内部锁好很多,但jdk6后者改进后很接近前者
• 公平性,是否允许新加入线程不按顺序立即获得锁
– 构造函数创建非公平锁(默认,统计学公平)和公平锁,内部锁都是非公平锁
– 公平锁适用于长时间持有锁或请求锁平均间隔较长的情况
– 非公平锁吞吐量更好,因为挂起并唤醒线程的开销巨大
ReentrantLock与内部锁
• 内部锁
– 不能中断正在等待锁的线程
– 并且请求锁失败时必须无限等待
– 必须在同个代码块释放锁
• RentrantLock具备了灵活性和相同的互斥和可见性语义,但是更危险
– 必须在finally块显示释放锁
– 要避免在try块之外抛出异常
– 如果对象会处于不一致的状态,可能还需要额外的try-catch或try-finally块
• 不要粗暴替换掉内部锁– synchronized标识简洁为人们熟悉
– 老代码基于synchronized的,混用两种同步机制容易混淆
– 忘记释放锁非常危险
– thread dump对synchronized支持更好,尤其是jdk5
– jvm现在和将来更可能针对synchronized优化,因为它是jvm内置的,Lock只是类库
• 只有当内部锁不够用,需要Lock高级特性时才应该使用显示锁
ReadWriteLock
• 读写锁
– 一个资源能够被多个读者访问,或者被一个写者访问,但两者不能同时进行
– 虽然看起来像两个锁,但其实是一个锁的两个不同视角
• 性能
– 多CPU而且读>>写的场景能够提高更好并发性提高性能
– 否则比独占锁稍差一些
• public interface ReadWriteLock
– Lock readLock()
– Lock writeLock()
• 可选特性
– 释放优先?读者闯入?可重入?降级?升级?
• ReentrantReadWriteLock
– 公平锁:选择等待时间最长的线程、读者不可闯入
– 非公平锁(默认):线程访问顺序不确定、可降级、不能升级(否则可能死锁)
– jdk5中,读取锁只维护活跃读者数量而不考虑它们身份;jdk6中,还可以追踪哪些
线程获取读锁,如可以区分线程是否首次请求该读锁避免公平读写锁死锁
自定义synchronizer
• synchronizer含义
– 依赖于状态的类,如果不满足状态的先验条件,类的
方法必须被阻塞
– 如FutureTask、Semaphore、BlockingQueue等
• 自定义synchronizer
– 依赖已有的synchronizer
– 采用底层机制
• 条件队列:内部条件队列和显式Condition
• AbstractQueuedSynchronizer(AQS)框架
• 条件队列
• AQS
条件队列
• 含义:可以让一组线程(等待集)以某种方式等待条件变成真;队列
的元素是等待条件的线程
• 条件谓词,由类的状态变量构成的表达式,操作和状态间的依赖关系
• 锁、条件谓词、条件队列的三元关系
– 条件谓词涉及状态变量;状态变量由锁保护
– 在验证测试条件谓词之前,必须先持有状态变量的锁
– 而且锁对象和条件队列对象必须是同一个对象
• 条件等待– 避免过早被唤醒,见内部条件队列的notifyAll
– 避免信号丢失,见前面活跃度失败问题
• 通知– 每当条件谓词变为真时,一定要确保发布通知
– 通知后要尽快释放锁,确保等待线程解除阻塞
• 内部条件队列和显示Condition
• synchronizer的继承安全问题
条件等待的best pratices
• 包括Object.wait和Condition.await,见list 14.7 p301
• 永远设置条件谓词,并在调用wait前测试条件谓词(避免
信号丢失问题)
• 永远在wait返回后再次测试谓词条件,并在循环中调用
wait(避免过早被唤醒的错误)
• 确保构成条件谓词的状态变量被锁保护,而且这个锁与条
件队列相关
• 调用条件队列api时要持有条件队列相关的锁
• 检查谓词条件后,执行被保护逻辑之前,不要释放锁
内部条件队列
• api:Object.wait/notify/notifyAll,必须在拥有对象锁情况下,才能调用
• wait
– 线程会自动释放锁,并请求os挂起当前线程
– 线程被唤醒时,会在返回前重新获得锁
– 线程重新获得锁与其他请求锁的操作的优先级一致
• notify,jvm挑选并唤醒一个条件队列中的线程
• notifyAll,唤醒条件队列中所有线程,可能导致过早唤醒
– 可能压根没有任何条件谓词为真
– 可能被别的线程先改变对象状态,条件谓词又为假
– 可能是共用条件队列的其他条件谓词为真
• notify vs notifyAll
– notifyAll比notify更容易保证正确性,但是可能导致大量的上下文切换和锁竞争
– 如果满足相同等待者和一进一出两个条件才能用notify(单一通知技术)
– 还可以用依据条件的通知技术来优化,见list 14.8 p304
– 单一通知和依据条件的通知都是优化手段,要遵循“先正确再按需优化”的原则
• 示例:使用wait和notifyAll实现可重关闭的阀门 list 14.9 p305
显式Condition
• 内部条件队列的缺陷
– 每个内部锁只能有一个关联条件队列
– 条件队列容易被锁模式暴露出去
• 显示Condition的优势
– 一个Lock可以有仸意个关联条件队列
– 可以选择是否可中断、是否公平、是否限时的条件队列
• 显示Condition的api
– Lock.newCondition
– Condition.await
• 可中断:await, await(time, unit), awaitNanos, awaitUntil
• 不可中断:awaitUninterruptibly
– Condition.singal和singalAll
• 显示Condition注意事项
– 必须满足锁、条件谓词、条件队列的三元关系,条件谓词涉及变量必须由Lock保护,
检查条件谓词和调用api时必须持有该Lock
– 不要误用了condition对象的wait/notify/notifyAll方法
– 需要高级特性时才应该选择显示Condition
示例:BoundedBuffer有限缓存
1. 将先验条件失败交给调用者处理
– GrumpyBoundedBuffer,见list 14.4 p294
– 非阻塞的,调用者必须自行处理先验条件失败
– 本质是把状态依赖性推卸给调用者,非常不好
2. 利用“轮询加休眠”的重试机制实现笨拙阻塞
– SleepyBoundedBuffer,见list 14.5 p295
– 阻塞的,封装了先验条件管理,简化了缓存的使用
– 不能及时唤醒
3. 利用Object内置的条件队列
– BoundedBuffer,见list 14.6 p298
– 语义同前者,具有更好的性能和响应性,用一个条件队列封装了两个条件谓词
– list 14.8 p304 使用依据条件通知技术的put操作减少notifyAll次数,优化性能
4. 利用显式条件队列
– ConditionBoundedBuffer,见list 14.11 p309
– 语义相同,易读,更好的性能(单一通知而不是notifyAll)
synchronizer的继承安全问题
• 使用依据条件的或单一通知技术会引入一些约束,如果在继承时破坏
了这些约束,可能引起子类线程安全问题
• 一个依赖于状态的类,要么将它的等待和通知协议(锁、条件队列甚
至状态变量)暴露并文档化(条件谓词和同步策略)给子类,要么完
全禁止子类改变它们(final关键字或隐藏相关对象)
• 封装条件队列,使用对象内部锁来保护对象自身的状态,如前面的
BoundedBuffer示例
• 入口协议和出口协议
– 入口协议,即条件谓词
– 出口协议,检查任何被操作改变的状态变量,如果引起条件谓词变真,
通知相关条件队列
– 如AbstractQueueSynchronizer,不让子类自行执行通知而是要求子类同步
方法的返回值说明它的动作是否可能已经阻塞一个或多个线程;通过显
示api方式来避免子类忘了执行通知的可能
AbstractQueuedSynchronizer(AQS)
• AQS是构建显式Lock和Synchronizer的框架(基类)
– 如list 14.12 p310 SemaphoreOnLock
• AQS解决了大量实现synchronizer的细节
– 等待线程的FIFO队列
– 仅有一个阻塞点
– 充分考虑了可伸缩性
• 基本操作– acquire和release,
– getState, setState, compareAndSetState
• 可扩展
– 独占访问: tryAcquire, tryRelease, isHeldExclusively
– 共享访问:tryAcquireShared, tryReleaseShared
AQS实例分析
• java.util.concurrent的synchronizer都是采用委托而不是直接继承自AQS
– 保持synchronizer接口的简洁性防止被误用
• OneShotLatch list 14.14 p 313(0=关闭,1=打开)
• ReentrantLock(0:锁可用,>1:被占用)
• Semaphore和CountDownLatch
• FutureTask(state对应任务运行、完成和取消)
• ReentrantReadWriteLock
– 一个state,16位为写锁计数,另16位为读锁计数
– 写锁独占访问,读锁共享访问
原子变量与非阻塞同步
• java.util.concurrent包为什么性能和伸缩性好?
– 就是因为大量使用原子变量和非阻塞的同步机制
• 非阻塞算法,使用底层原子化机器指令取代锁保证数据在
并发访问下的一致性,比如Compare-And-Swap(CAS)– 多线程竞争资源时不会发生阻塞
– 更够进行更精巧的优化,减少调度开销
– 避免死锁和其他活跃度问题
• 原子变量
– 能高效地构建非阻塞算法
– 当做更好的volatile变量,因为提供原子更新操作
锁的劣势
• JVM能够对非竞争锁的获取和释放进行优化
• 多个线程请求锁时,现代JVM有可能不请求os挂起线程,它可以利用运行统计
的占用锁的时间长短,来决定是挂起还是自旋等待
• 基于锁的并且操作过度细分的类,会频繁地发生锁竞争,调度占去了大多数时间
• volatile是不错的工具,但是只能保证可见性,不能构建原子化的复合操作
• 线程在等待锁时,不能做任何事情
• 如果线程在持有锁情况下发生了延迟(如页错误、调度延迟等),那么其他
需要该锁的线程都不能前进了
• 如果阻塞线程是优先级高,持有锁的线程优先级较低,会造成性能风险优先级倒置
• 即使是优先级更高的线程,仍然需要等待锁被释放,导致它的优先级降低到
与低优先级线程同一水平
• 如果持有锁的线程发生了永久性阻塞(无限循环,死锁,活锁等),所有等
待该锁的线程都不会前进了
• 加锁对于细分操作而言,仍然是重量级的机制,如递增计数器
硬件对并发的支持(CAS)
• 早期CPU的原子化test-and-set, fetch-and-increment和swap指令,用来实现互斥
和并发对象
• 现代CPU有原子化读-改-写指令,如IA32的CAS和power的load-linked/store-conditional,JVM用来实现锁和并发数据结构
• 独占锁是悲观的,假设最坏的情况,通过加锁来能避免其他线程的打扰
• CAS和自旋锁都是乐观技术,做最好的打算,能够发现其他线程的打扰
• CAS抱着成功的希望更新,发现其他线程的打扰时更新失败而不是阻塞
– 失败意味着更新确实失败(可以重试),或者更新被别人完成(什么都不用做)
– 性能变化很大,单CPU下约1个时钟周期,多CPU下非竞争CAS约10~150个时钟周期
– 如果平台支持CAS,jvm运行时会把CAS解释成恰当的机器指令;否则采用自旋锁
– 缺点是需要调用者处理竞争,难以正确地构建外围算法
• 应用级看起来很长的代码路径,在考虑JVM和OS时可能变得更短
– 加锁需要遍历JVM中复杂代码路径,至少需要一次CAS,还可能引起系统级加锁、
线程挂起和上下文切换;最优情况下获取释放无竞争锁开销大约是CAS的两倍
– 执行CAS不会调用到JVM代码、系统调用或调度活动
– 示例 list 15.2 p323 非阻塞计数器
Read-modify-write指令比较
• fetch-and-add
– 早期CPU提供,功能最弱,只能同步两个以内的线程
• cas
– 有aba问题,三个寄存器(address, oldValue, newValue),用于X86等CISC
• load-linked/store-conditional, aka ll/sc
– 同步功能最强最好,一对指令,ll取出指定内存地址的原值,接下来sc时,如果在ll到sc之间没有对该内存单元的更新,则执行成功,反之失败
– 通常实现中,并不是间隙中没有更新就一定保证成功执行sc,因为间隙中很可能被中断、异常、进程切换等打断,在这些情况下也会失败,学术上称之为weak ll/sc
– 无aba问题,两个寄存器(address, value)
– 与cas相比,更适吅设计load-store architecture,如RISC
• 参考
– 谈谈锁和原子操作,http://fire3.info/23.html
– Maurice Herlihy的经典论文Wait-Free Synchronization
原子变量类
• 更新原子变量的快速路径比获取锁的快速路径更快;而慢速路径通常会更快
• 原子变量类继承自Number,而不继承包装类,因为它们是可变对象,而且也
不适吅做哈希容器的key
– 计量器,AtomicInteger/Long/Boolean/Reference,还能模拟其它基本类型
– 域更新器,AtomicReferenceFieldUpdater,更新既有的volatile域
– 数组,AtomicInteger/Long/ReferenceArray,每个元素都有volatile语义
– 复合变量
• 原子变量可以当做更好的volatie使用,见 list 15.3 p 326 CasNumberRange
• 锁与原子变量性能比较
– 激烈竞争下,锁胜过原子变量,因为锁通过挂起线程减少CPU的利用和总线通信量
– 中等竞争下,原子变量具有更好的伸缩性
– 现实中,除了竞争还有更多工作要完成,中等竞争情况更普遍
– 类似地,交通灯适用于拥挤的路口,而环岛适用于低拥堵;以太网适用于低通信
量而令牌网适用于高通信量下
非阻塞算法
• 非阻塞算法,一个线程的失败或挂起不会引起其他线程失败或挂起
• 锁自由算法,算法的每一步都有一些线程能够继续执行
• 基于CAS构建的算法,即是非阻塞又是锁自由的
• 示例
– 非阻塞栈,采用Treiber算法,见 list 15.6 p331
– 非阻塞链表,Michael-Scott算法,见 list 15.7 p 334
• 原子化域更新器– 利用反射对已有的volatile进行更新
– 可以减少创建Atomic类进一步提高性能
• ABA问题
– 算法中如果进行自身链接节点对象的内存管理可能出现ABA问题
– JVM采用内部数据保存引用和版本号,更新时同时这一对值,如AtomicStampedReference和AtomicMarkableReference
• ABA问题JVM没采用的解决办法
– 利用CPU的double-length CAS(CAS2或CASX)指令,非DCAS
– 保存指向对象在全局freelist的索引和版本号,如32 bit CAS, 16 bit存索引, 16 bit存版本号
– Safe Memory Reclamation
wiki里的非阻塞式算法
• wiki定义
– 多线程竞争共享资源时不用因为互斥而全部暂停,多利用原子的读-改-写指令实现
• 无等待– strongest non-blocking guarantee of progress,system-wide progress
– combining guaranteed system-wide throughput with starvation-freedom
• 锁自由
– per-thread progress non-blocking guarantee, all wait-free algorithms are lock-free
– allow individual threads to starve but guarantees system-wide throughput
– generally, a lock-free algorithm can run in four phases: completing one's own operation, assisting an obstructing operation, aborting an obstructing operation, and waiting
– when to assist, abort or wait when an obstruction is met is the responsibility of a contention manager;
– correct concurrent assistance is typically the most complex part of a lock-free algorithm, and often very costly to executewhen to assist, abort or wait when an obstruction
• 无隔离, also called optimistic concurrency control– weakest natural non-blocking progress guarantee, all lock-free are obstruction-free
– dropping concurrent assistance; only demand that any partially-completed operation can be aborted and the changes made rolled back
– Preventing the system from continually live-locking is the task of a contention manager
Java存储模型=JMM
• JMM是多线程程序设计高层原则的保证,如安全发布、规约、遵守同步策略
• JMM是神马
– 有个赋值语句:aVariable = 3;JMM要回答在什么条件下读取aVariable的线程会看
到3这个值?
– java语言规范规定了JVM要维护内部线程顺序化语义:只要程序的最终结果等同于
它在严格顺序化环境中执行的结果,就允许编译器或CPU的优化行为
– 规定jvm的一种最小保证:什么时候写入一个变量对其他线程可见;便于在可预言
性需要和并发程序开发简易性之间取得平衡
• CPU对性能提升除了提升时钟频率,还可以提高并行性(管道超标量体系结构
执行单元,动态指令调度,试探性执行以及多级存储缓存)
• 缺少同步导致线程无法立即—甚至永远—看不到另一个线程最新操作的结果
– 编译器指令重排序
– 编译器把变量存储在寄存器而不是内存
– 处理器乱序或并发执行指令
– 处理器缓存的修改提交到内存的次序
– 处理器本地缓存中的值对别的处理器不可见
硬件的存储模型
• 可共享内存的多处理器体系架构中,每个处理器都有自己的缓存
– 缓存周期性与主存协调一致
– 提供不同级别的缓存一致性或最小保证(几乎任何时候都允许不同处理
器在相同存储位置看到不同值)
• 保证每个处理器在任意时间都能知道其他处理器的代价非常高昂
– 现代处理器会牺牲存储一致性换取性能
– memory barriers或fences指令可在共享数据时提供存储协调保证
– JVM提供JMM屏蔽底层硬件存储模型的差异性
• 理想的顺序化一致性模型
– 操作执行顺序是唯一的,按照代码里的先后顺序
– 变量每次读操作都能得到执行序列上该变量的最新值
– 没有哪个现在多处理器架构能够提供这种模型
指令重排序
• 各种能够引起操作延迟或乱序执行的原因
都可以归结为一类重排序
• 在没有正确同步情况下即使最简单的并发
程序推断它的行为也是困难的,见list 16.1
• 同步抑制了编译器、硬件和运行时对存储
操作的各种重排序,否则将会破坏JMM提供的可见性保证
JMM简介-概念
• JMM是通过动作actions进行描述的;所谓动作包括变量的
读和写、监视器加锁和释放锁、线程的启动和拼接等
• happens-before(<):JMM为动作定义了的偏序关系,
无论动作A和B是否在同一线程中,只要A < B就能保证A操作对B的可见性;否则,JVM可以对它们任意重排序
• 偏序关系是集吅上的一种反对称、自反的传递的关系,不
过并不是集合中任意两元素都必须满足要么x < y或y < x,否则就是全序关系
• 数据竞争data race:当一个变量被多个线程读取,至少被
一个线程写入时,如果读写操作并未依照happens-before排序就会产生数据竞争
• 正确同步的程序:没有数据竞争,表现出顺序一致性
JMM的八条happens-before法则
• 程序次序法则
– 线程中每个动作A都happens-before该线程中的每个动作B,其中动作B都出现在动作A之后
• 监视器锁(包括显式锁)法则,满足全序– 对一个监视器锁的解锁happens-before于每个后续对同一个监视器的加锁
• volatile (包括原子变量)法则,满足全序
– 对volatile的写入操作happens-before于每个后续对同一个域的读操作
• 线程启动法则
– 一个线程里,Thread.start的调用happen-before每个启动线程中的动作
• 线程终结法则
– 线程中任何动作都happens-before于其他线程检测到这个线程已终结或者从Thread.join调用中
成功返回或Thread.isAlive返回false
• 中断法则
– 一个线程调用另一个线程的interrupt happens-before于被中断线程发现中断
• 终结法则
– 一个对象的构造函数结束happens-before于这个对象的finalizer开始
• 传递法则
– 如果A happens-before B,且B happens-before C,那么A happens-before C
利用JMM驾驭在同步之上
• happens-before有点隐式同步的感觉
• 在同步机制之上提供可见性保证,结合程序次序
法则和另外的法则(通常是监视器锁和volatile法则)对访问变量操作排序,否则就用锁保护它
• 当然这属于高端技术对语句顺序是敏感的,容易
出错,应该被留作性能关键类最后的优化手段,
见list 16.2利用程序次序法则和volatile法则
• JDK里面利用这种驾驭技术在同步之上保证可见
性,提供了更较高层次的happens-before法则
JDK担保的happens-before
• AQS法则– tryReleaseShared happens-before tryAcquireShared
• 线程安全容器法则
– 将一个条目置入线程安全容器happens-before于另一个线程从容器获取条目
• Latch法则
– 执行CountDownLatch.countDown happens-before线程从Latch的await中返回
• Semaphore法则
– Semaphore.release happens-before 同一个Semaphore.acquire
• Future法则
– Future代表的仸务所发生的动作happens-before另一个线程从Future.get返回
• Executor法则
– 向Executor提交Runnable或Callable happens-before开始执行任务
• Barrier法则
– 线程到达CyclicBarrier或Exchanger happens-before 相同关卡中的其他线程释放
– CyclicBarrier使用barrier动作,到达barrier < 关卡动作,关卡动作 < 线程从关卡中释放
• 静态初始化法则
– 加载类 happens-before 静态初始化 happens-before 被仸意线程使用
用happens-before看对象发布
• 不安全的发布
– 不正确发布带来风险的本质:发布共享对象和从另一个线程访问它之间缺少
happens-before偏序
– 在没有充分同步情况下发布对象,会导致另外线程看到部分创建的对象(如list 16.3),访问到过期值,因为Object o = new Object()包含两步操作:新对象初始化
(写入新对象的域)和引用发布两步(写入新对象的引用)
• 安全发布
– 除了不可不变对象以外,使用被另一个线程初始化的对象是不安全的,除非对象
的发布是happens-before消费线程使用它
– happens-before可以用来实现安全发布正像全面介绍的安全发布技术
• happens-before提供比安全发布更强的可见性和排序性
– 安全发布仅表示X从线程A到B是可见的
– happens-before除保证X的可见性,还保证线程A之前操作的其他变量对B的可见性
– 但是用happens-before太晦涩了,采用安全发布更适合普通程序设计
再论惰性初始化
• 不安全的初始化,list 16.3– 存在竞争条件和数据竞争
• 安全的同步的被动初始化,list 16.4– 同步整个方法
• 安全的非同步的主动初始化,list 16.5– 利用静态初始化
• 安全的非同步的被动初始化,list 16.6– 利用内部类+静态初始化
• 不安全的DCL(Double-checked Locking),list 16.7– 只解决竞争条件,没解决数据竞争,存在不可见问题
• 安全的DCL– 在list 16.7基础把私有变量变成volatile变量
初始化安全性
• 初始化安全性可以让正确创建的不可变对象在没有同步情
况下被安全地跨线程共享,而不管他们是怎么发布的(即
使发布时存在数据竞争),如String
• 正确创建的对象,初始化安全性可以抑制重排序,保证无论它如何发布
– 所有线程都将看到构造函数设置的final域值
– 构造函数中仸何可以通过final域写入的变量对其他线程也可见,
见list 16.8
• 初始化安全性保证只有通过final域触及的值在构造函数完
成时才是可见的;非final触及值或者创建后可能改变的
值,必须用同步保证可见性
Java Thread States
Java Thread Lifecycle
并行编程基础概念回顾
• 同步、阻塞、互斥、串行
– 同步不一定阻塞,比如原子操作,非阻塞算法
– 同步就是串行
– 互斥才会形成阻塞
• 线程安全
– 原子性、内存可见性、竞争条件、数据竞争、设计原则
• 活跃度
– 死锁、活锁、自旋、忙等
• 并行度、吞吐量、可伸缩性、Amdahl定律
• 非阻塞算法、Double-Check-Lock问题、ABA问题、悲观与乐观锁
• 仸务生命周期管理、中断处理
java并行编程api汇总
• 线程
– Thread, ThreadGroup, ThradLocal, Timer(bad), Runnable, Callable
• 同步– happens-before,volatile,synchronized,显式Lock(ReentrantLock, ReadWriteLock, FileLock)
• 同步容器
– Collections.synchronusXXXX,Vector, HashTable等
• 并发容器– ConcurrentXXXX, CopyOnWriteXXXX
• synchronizer– AQS, SynchronusQueue, BlockingQueue, Deque, CountDownLatch, Barrier, Semaphore, FutureTask
• 原子类– AtomicInteger, AtomicLong, AtomicReferernfce
• Executor– ThreadExecutorPool, Executors, ExecutorService
• jdk工具– jps, jstack, jconsole, jmap
java并行编程背后细节回顾
• 硬件
– 原子操作,memory barrier,cas指令
• JVM
– server参数
– 编译优化和运行时优化
– 指令重排序,乱序执行,逃逸分析
• JMM
– 八条happens-before规则
JDK补遗
• ThreadLocal怎么实现的?
• 仸务执行的首要抽象不是Thread而是Executor
• Thread.stop和Thread.suspend缺陷,http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDepreca-tion.html
• Join api不足,因为无论join是否成功完成,在jmm中都会相应存在内
存可见性,但是join本身不会返回表示它成功与否的状态
• Swing把大多数可视化组件分为模型和视图两个对象,而且模型被更
新后会直接调用视图的fireXXX方法触发视图更新事件,而不是把它再
放入Swing事件队列中,所以模型和视图更新都在一次模型事件处理
的EDT中
• System.identityHashCode即Object.hashCode,冲突概率很低
JVM探密
• 支持编译器对指令重排序
• 32位变量读取和写入是原子的,64位不是• 只有构造函数返回才表示对象构建完成,哪怕在函数最后一行也没有
被正确构建• 构建一个对象的CPU和内存操作?• Final的特殊语义?
• -server选项开启更多优化与生产环境一致
• static初始化的内在同步?
• 每个对象都有一个内部锁方便了编程,但是强迫JVM实现者权衡对象的大小与锁的性能,JCIP作者觉得这很糟糕
• 每个线程都维护者两个执行栈,一个用于java代码一个用于本地代码;JVM默认的组合栈大小为512K(-Xss JVM或者Thread构造函数可以修改),如果每个栈分配232B,总线程数量为几k~几w
• JVM会在所有非后台线程结束后退出,每个线程退出时JVM都会检查运行中线程的详细清单,如果仅剩下后台线程就会触发正常退出
JVM探密2
• 线程API定义了10个优先级别,并对应到操作系统的调度优先级,但
这种映射肯定不是一一对应的,所以优先级这个API是平台相关的。
默认优先级是Thread.NORM_PRIORITY
• Thread.yield和sleep(0)语义是未定义的,JVM可以仸意实现它们,尽量
一些JVM是根据优先级,把当前线程放在队尾并产生与其他线程相同
的优先级
• jvm优化
– 逸出分析,识别本地对象引用没有暴露在堆中
– 锁粗化,把邻近的synchronized块用相同锁合并起来,在synchronized内部利用启发式而不是指令方式产生同步开销,这不仅减少同步开销,还能给予优化者更大的代码块,做进一步优化
– 线程竞争锁时可以不挂起线程,而是利用积累的统计,根据占用锁时间
的长短来决定是挂起还是自旋等待
• 挂起唤醒线程需要做哪些事情?