Java并发编程(下)
2025-03-23 09:45:38 0 举报
AI智能生成
Java并发编程
作者其他创作
大纲/内容
取消与关闭
任务取消
中断
不可靠的取消操作将把生产者至于阻塞的操作中
如果生产者的速度超过消费者, 并且队列北填满, put方法也会被阻塞, 可能这个方法永远不会被终止
如果生产者的速度超过消费者, 并且队列北填满, put方法也会被阻塞, 可能这个方法永远不会被终止
阻塞库方法, 都会检查线程何时中断,并且在发现中断时提前返回
通常, 中断是实现取消的最合理方式
中断策略
最合理的中断策略是某种形式的线程级取消操作或服务级取消操作: 尽快退出,在必要时进行清理, 通知某个所有者该线程已退出\
如果出了将InterruptedExcpetion传递给调用者之外还需要执行其他操作,那么应该在捕获异常后恢复中断状态
Thread.currentThread().interrupt();
Thread.currentThread().interrupt();
响应中断
有2种策略可以用于处理InterruptedException
传递异常, 从而使你的方法也成为可中断的阻塞方法
恢复中断状态,从而使调用栈种的上层代码能够对其进行处理
计时运行
许多问题永远无法解决,如果能指定"最多花10分钟搜索答案"或者"枚举出在10分钟累哦能找到的答案", 那么则需要计时运行
在外部线程中安排中断
由于timedRun可以从任意一个线程调用,因此它无法知道这个调用线程的中断策略
由于timedRun可以从任意一个线程调用,因此它无法知道这个调用线程的中断策略
这个示例的代码解决了前面示例中的问题,但由于它以来于一个限时的join,因此存在join的不足: 无法知道执行控制是因为线程正常退出而返回还是因为join超时返回
通过Future来实现取消
当Futrue.get抛出InterruptException或TimeoutException, 如果你知道不再需要结果,那么就可以调用Future.cancel来取消
停止基于线程的服务
示例:日志服务
不支持关闭的生产者-消费者日志服务
这种直接关闭的方式,会丢失那些正在等待被写入到日志的信息
这种直接关闭的方式,会丢失那些正在等待被写入到日志的信息
通过一种不可靠的方式为日志服务增加关闭支持
设置某个"已请求关闭"标志,以避免进一步提交日志消息, 但是这样有概率使线程阻塞在put方法,并无法解除阻塞
设置某个"已请求关闭"标志,以避免进一步提交日志消息, 但是这样有概率使线程阻塞在put方法,并无法解除阻塞
向LogWriter添加可靠的取消操作
关闭ExecutorService
ExecutorService会一直等到队列中的所有任务都执行完成后才关闭
毒丸对象
当得到这个对象时,立即停止
通过"毒丸"对象关闭服务
处理非正常的线程终止
我们都需要在任何时候考虑捕获RuntimeException
典型的线程池工作者线程结构
如果检查到异常,那么它将使线程终结,但会首先通知框架该线程已经终结
如果检查到异常,那么它将使线程终结,但会首先通知框架该线程已经终结
只有通过execute提交的任务,才能抛出异常, 而submit提交的任务的异常被认为是返回状态的一部分
JVM关闭
关闭钩子
指通过Runtime.addShutdownHook注册的但尚未开始的线程
通过注册一个关闭钩子来停止日志服务
守护线程
JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程
我们应该尽可能少地使用守护线程, 例如守护线程中执行可能包含I/O操作的任务, 那么将是危险的
守护线程最好用于执行"内部"任务,例如周期性地从内存的缓存中移除逾期的数据
终结器
一些资源,如文件句柄,套接字句柄,当不需要他们时,必须显式交换给操作系统
垃圾回收期对定义了finalize方法的对象进行特殊处理
避免使用终结器
线程池的使用
在任务与执行策略之间的隐式解耦
线程饥饿死锁
如果一个任务依赖于其他任务,那么可能发生死锁
在单线程Executor中任务发生死锁
运行时间较长的任务
时间过长的任务使线程池响应变得糟糕, 可以通过Thread.join,BlockingQueue.put等设置超时时间
设置线程池大小
计算密集型的任务, 在用户N个cpu的系统上, 线程池大小为N+1
包含I/O操作的任务, 线程池数量=CPU数量*CPU使用率*(1+等待时间比例)
计算每个任务队该资源的需求量,然后用该资源的可用总量除以每个任务的需求量,得出线程池大小的上限
配置ThreadPoolExecutor
线程的创建与销毁
通过调节线程池的基本大小和存活时间,可以帮助线程池回收空闲线程占有的资源
管理队列任务
只有当任务互相独立时,为线程池或者工作队列设置界限才是合理的
如果任务之间存在依赖性,那么有界的线程池或队列就可能导致线程"饥饿"死锁问题,此时应该使用无界队列
饱和策略
不同的饱和策略: AbortPolicy, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy
使用Semaphore来控制任务的提交速率
线程工厂
如果希望给线程一个更有意义的名称, 用来解释线程的转储信息和错误日志
在调用构建函数后再定制ThreadPoolExecutor
在Executors中包含一个unconfigurableExecutorService工厂方法, 该方法对一个现有的ExecutorService进行包装, 使其只暴露出ExecutorService方法,因此不能对他进行配置
拓展ThreadPoolExecutor
ThreadPoolExecutor是可拓展的,它提供了几个可以在子类化中改写的方法:beforeExecutue, afterExecute 和 terminated
给线程池添加统计信息
增加了日志和计时等功能的线程池
递归算法的并行化
将串行递归转换为并行递归
活跃性,性能与测试
死锁
锁顺序死锁
2个线程尝试以不同的顺序来获取相同的锁,导致死锁
如果所有线程以固定的顺序获取锁,那么程序中就不会出现锁顺序死锁的问题
动态的锁顺序死锁
所有线程似乎都是按照相同的顺序获取锁, 但事实上锁的顺序取决于传递给transferMoney的参数顺序.
若同时执行, 则容易发生死锁
通过对比fromAcct和toAcct的hashcode, 判断先锁哪个账户, 保证按照相同的顺序获得锁
在协作对象之间发生死锁
同时在调用setLocation和getImage的时候, 不能保证相同顺序获得锁
如果在持有锁时调用某个外部方法, 那么将出现活跃性问题. 在这个外部方法中可能会获取其他锁, 或者阻塞时间过长, 导致其他线程无法及时获得当前被持有的锁
开放调用
通过公开调用来避免在项目协作的对象之间产生死锁
死锁的避免与诊断
支持定时的锁
给显式锁指定一个超时的时限, 使在发生意外情况后重新获得控制权
通过线程转储信息来分析死锁
其他活跃性危险
饥饿
要避免使用线程优先级, 因为这样会增加平台依赖性, 并可能导致活跃性问题
在大多数并发应用程序中,都可以使用默认的线程优先级
糟糕的响应性
不良的锁管理可能导致糟糕的响应性, 例如某个线程长时间占用一个锁
活锁
多个相互协作的的线程都对彼此进行响应从而修改各自的状态, 并使任何一个线程都无法继续执行
在并发应用中,通过等待随机长度的时间和回退可以有效避免活锁的发生
性能与可伸缩性
对性能的思考
性能与可伸缩性
当增加计算资源时,程序的吞吐量或者处理能力能响应增加
评估各种性能的权衡因素
避免不成熟的优化, 首先使程序正确, 然后在提高运营速度---如果它运营还不够快
以测试为基准, 不要猜测
Amdahl定律
当N趋于无穷大,最大的加速比趋近于1/F
如果程序有50%的计算需要串行执行, 那么最高的加速比只能是2(无论有多少个线程可用), 如果在程序中有10%的计算需要串行执行, 那么最高的加速比将近10
线程引入的开销
上下文切换
如果可运行的线程数大于CPU的数量, 那么操作系统最终会将某个正在运营的线程调度出来, 从而使其他线程也能够使用CPU
上下文切换的实际开销会随着平台的不同变化,按照经验来看, 大多数通用的处理器中, 上下文切换的开销相当于5000-10000个时钟周期,相当于几微秒
内存同步
synchronized和volatile提供可见性, 非竞争同步带来的消耗大约20-250个时钟周期
不用过度担心非竞争同步带来的开销
阻塞
当在锁上发生竞争, 失败的线程会阻塞
JVM在实现阻塞行为时,采用自旋等待, 或操作系统挂起的方式
如果等待时间短, 则适合自旋等待, 如果等待时间长, 则适合线程挂起的方式
减少锁竞争
减小锁的范围
减少锁持有的时间, 提高并行度
减小锁的粒度
降低线程请求锁的频率
锁分段
如ConcurrentHashMap, 通过锁分段的方式, 提高并行度
避免热点域
通过缓存热点数据减少锁的获取
监控CPU的利用率
负载不充足
I/O密集
外部限制
锁竞争
向对象池说"不"
当线程分配新对象时,基本上不需要在线程之间协调,因为对象分配器通常会使用线程本地的内存块
如果线程从对象池中请求一个对象,那么就需要通过某种同步来协调对象池数据结构的访问
虽然这看似一种性能优化的技术,但实际上会导致可伸缩性问题, 对于性能优化来说,用途是有限的
0 条评论
下一页