并发编程实战学习笔记(七)——避免活跃性问题
2017-03-26 11:05
495 查看
锁顺序死锁
定义
试图以不同的顺序去获得相同的锁,就可能会产生死锁解决办法
如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题动态的锁顺序死锁
原因
锁顺序本身是动态的,无法通过相同的顺序来避免死锁问题解决办法
通过一致哈希算法或者其它方式来统一锁顺序,使未知顺序变为已知顺序。对于极少数的哈希冲突,可以使用“加时赛”锁来解决private static final Object tieLock = new Object(); public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException{ class Helper{ public void transfer throws InsufficientFundsException{ if(fromAcct.getBalance().compareTo(amount) < 0){ throw new InsufficientFundsException(); }else{ fromAcct.debit(amount); toAcct.credit(amount); } } } int fromHash = System.identifyHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); if(fromHash < toHash){ synchronized(fromAcct){ synchronized(toAcct){ new Helper.transfer(); } } }else if (fromHash > toHash) { synchronized(toAcct){ synchronized(fromAcct){ new Helper().transfer(); } } } else { synchronized(tieLock){//加时赛锁来解决问题 synchronized(fromAcct){ synchronized(toAcct){ new Helper().transfer(); } } } } }
在协作对象之间发生的死锁
如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其它锁(这可能会产生死锁),或者阻塞时间过长,导致其它线程无法及时获得当前被持有的锁。锁顺序死锁可能以这种方式隐式出现
开放调用
如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用简化并发程序分析难度的可行思路
虽然在没有封装的情况下也能确保线程安全的程序,但对一个使用了封装的程序进行线程安全分析,要比分析没有使用封装的程序容易得多。分析一个完全依赖开放调用的程序的活跃性,要比分析那些不依赖开放调用的程序的活跃性简单。
开放调用的改造思路
锁消除;去掉本来要持有的锁细粒度锁;只锁一小块,调用其它方法时就不需要锁了。
资源死锁的类型
独占类型的访问都可以和加锁操作类比,看起来就像需要获得锁才能访问。如果一个任务需要连接两个数据库,并且在请求这两个资源时不会始终遵循相同的顺序,那么线程A可能持有与数据库D1的连接,并等待与数据库D2的连接,而线程B持有D2的连接并等待与D1的连接。资源池越大,就越不容易出现这种类型的死锁。
线程饥饿死锁。如果某些任务需要等待其它任务的结果,那么这些任务往往是产生线程饥饿死锁的主要来源,有界线程池/资源池与相互依赖的任务不能一起使用。
死锁的避免思路
尽量依赖开放调用。如果一个程序每次至多只能获得一个锁,那么就不会产生锁顺序死锁。如果必须获取多个锁,那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入正式文档并始终遵循这些协议。
显示使用Lock类中的定时tryLock功能来代替内置锁机制。
定时锁的优点
当定时锁失败时,你并不需要知道失败的原因。至少你能记录所发生的失败,以及关于这次操作的其它有用信息,并通过一种更平缓的方式来重新启动计算,而不是关闭整个进程。如果在获取锁时超时,那么可以释放这个锁,然后后退并在一段时间后并再次尝试,从而消除了死锁发生的条件,使程序恢复过来。(这项技术只有在同时获取两个锁时才有效,如果在嵌套的方法调用中请求多个锁,那么即使你知道已经持有了外层的锁,也无法释放它。)
饥饿
当线程由于无法访问它所需要的资源而不能继续执行时,就发生了“饥饿”,引发饥饿的最常见资源就是CPU时钟周期。问题
我们尽量不要改变线程的优先级。只要改变了线程的优先级,程序的行为就将与平台相关解释
在Thread API中定义了10个优先级,JVM根据需要将它们映射到操作系统的调度优先级。这种映射与特定平台相关的,因此在某个操作系统中两个不同的Java优先级可能被映射到同一个优于级,而在另一个操作系统中则可能被映射到另一个不同的优先级。在某些操作系统中,如果优先级的数量少于10个,那么有多个java优先级会被映射到同一个优先级。活锁
活锁是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。过度错误恢复代码
程序捕捉到错误消息时,会重新把错误消息放入队列或者直接重复操作,这种过度的错误恢复机制,直接导致程序一直在“处理-错误-发现-重新处理”的循环中无法跳出解决办法就是识别出这种消息,并且跳过处理
不恰当地礼让
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。这就像两个过于礼貌的人在半路上面对面地相遇,他们彼此都让出对方的路,然而又在另一条路上相遇。因此他们就这样反复地避让下去。要解决这种活锁问题,需要在重试机制中引入随机性。在并发应用程序中,通过等待随机长度的时间和回退可以有效避免活锁的发生。
相关文章推荐
- 并发编程实战学习笔记(九)-显式锁
- 并发编程实战学习笔记(二)——对象的共享
- Java 并发编程实战学习笔记
- 并发编程实战学习笔记(十)-构建自定义的同步工具
- java 并发编程学习笔记之volatile意外问题的正确分析解答
- 并发编程实战学习笔记(三)——基础构建模块
- Java 并发编程实战学习笔记——路径查找类型并行任务的终止
- 并发编程实战学习笔记(六)——线程池的使用
- Java并发读书学习笔记(八)——避免活跃性危险
- Java 并发编程实战学习笔记——寻找可强化的并行性
- Java 并发编程实战学习笔记——串行任务转并行任务
- 并发编程8.避免活跃性问题与性能思考
- 并发编程实战学习笔记(十一)-原子变量与非阻塞同步机制
- 并发编程实战学习笔记(八)——性能与可伸缩性
- Java 并发编程实战学习笔记——CountDownLatch的使用
- 并发编程实战学习笔记(五)——取消与关闭
- Java并发学习笔记(12)避免活跃度危险(死锁)
- linux网络编程学习笔记之五 -----并发机制与线程池
- java并发编程实践学习笔记
- [转]JAVA并发编程学习笔记之Unsafe类