线程死锁

死锁通常发生在多个线程由于环路依赖关系而永久等待,如当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),但分离锁的缺点是想要独占访问整个容器更困难,比如重排时需要获取所有锁。

采用协调机制代替独占锁

使用并发容器,读写锁(允许多读,但是写则必须独占锁),不可变对象,原子变量、

减少上下文切换的开销

在线程运行和阻塞两个状态转换需要使用上下文切换,因此要尝试减少切换上下文的频率