线程死锁
死锁通常发生在多个线程由于环路依赖关系而永久等待,如当A线程占有锁M,A想要获取锁N,但B线程持有锁N,并尝试获取锁M,则两个线程将永远阻塞等待,形成死锁
数据库系统检测死锁和恢复:通过监测正在事务之间的等待关系(如事务A等待事务B,A -> B)形成的有向图,若发现环,则会选择一个事务退出,使得其他事务能继续进行,然后再重新执行这个被退出的事务
死锁类型
锁顺序死锁:两个线程试图通过不同顺序获取多个相同的锁。可以通过检查是否有嵌套,并制定好锁顺序,可以使用加时赛锁来规避
协作对象间死锁:在持有锁的时候,调用外部方法,而该外部方法又去获取其他锁。因此尽量多设计一些开放调用(不持有锁),利用同步块代替同步方法减小持有锁的粒度,来减小死锁风险。
资源死锁:比如数据库连接池(信号量)
饥饿死锁:单线程串行
死锁避免和诊断
- 一次只获取一个锁,则不会产生顺序死锁,文档化锁顺序协议
- 细化锁的范围,尽量用开放调用
- 尝试超时机制,比如用Lock类替换内部锁
- JVM通过线程转储(thread dump)识别死锁发生,搜索“正在等待”有向图中的环
线程饥饿
线程访问所需资源(CPU周期)时被永久拒绝。比如线程优先级太低一直无法获取到CPU执行周期
活锁
线程没有阻塞,但是不能继续向前执行。比如过度重试机制和两个线程之间相互让路的情况。可以试着引入一些随机性
引入线程的开销
切换上下文
上下文切换的开销相当于5000到10000个时钟周期或几微秒
内存同步
synchronized和volatile利用存储关卡(memory barrier)来刷新硬件写缓存,并延迟执行的传递,抑制了编译器优化,操作不能重排序
阻塞
当多个线程竞争锁而发发生阻塞,被阻塞的线程会自旋等待(不断尝试)或挂起
提高并发性能
减少锁竞争
减小持有锁的时间
- 把无关代码移出同步块,尤其是潜在的阻塞操作如IO,
- 利用线程代理的方式,比如采用线程安全的底层容器来代理该对象所有线程安全的职责
减小请求锁的频率
采用分离锁,即用多个相互独立的锁守护多个独立的状态变量(如ConcurrentHashMap),但分离锁的缺点是想要独占访问整个容器更困难,比如重排时需要获取所有锁。
采用协调机制代替独占锁
使用并发容器,读写锁(允许多读,但是写则必须独占锁),不可变对象,原子变量、
减少上下文切换的开销
在线程运行和阻塞两个状态转换需要使用上下文切换,因此要尝试减少切换上下文的频率