Java锁的开销和优化
降低高并发程序锁竞争的方法
1 相关概念
- Mutable Shared Objects(可变共享对象) --- objects that are accessible by more than one thread and those objects can be changed
- Thread Dump(线程转储) ---
- 锁的粗化(lock coarsening) --- 把邻近的 synchronized 块用相同的锁合并起来,以减少不必要的锁的获取和释放。
- 分拆锁 (lock splitting) ---如果一个锁守护多个相互独立的状态变量,你可能能够通过分拆锁,使每一个锁守护不同的变量,从而改进可伸缩性。通过这样的改变,使每一个锁被请求的频率都变小了。分拆锁对于中等竞争强度的锁,能够有效地把它们大部分转化为非竞争的锁,使性能和可伸缩性都得到提高。
- 分离锁 (lock striping) --- 分拆锁有时候可以被扩展,分成若干加锁块的集合,并且它们归属于相互独立的对象,这样的情况就是分离锁。例如,ConcurrentHashMap 的实现使用了一个包含 16 个锁的数组,每一个锁都守护 HashMap 的 1/16 。
2 并发相关的性能问题
- Thread deadlocks
- Thread gridlocks
- Thread pool sizing issues
2.1 死锁
死锁症状:JVM将最终用完或使用大部分进程,应用程序看起来完成的工作越来越少,但server的CPU并没有充分利用;如果进行线程转储,能从中找到线程死锁的报告。
死锁影响:应用程序将停止处理业务逻辑,更糟糕的是解决这个问题的唯一办法是重启JVM;在非生产环境中很难重新,因而排查非常困难。
死锁排查(TroublShooting):capturing a thread dump while two threads were deadlocked and then examining the stack traces of the deadlocked threads.
2.2 线程池大小问题
这个问题看2个方面
- 线程池利用率
- CPU利用率
如果线程池利用率很高(已到或将要到100%),而且有未处理的请求,CPU利用率不高,那么很可能是线程池配置线程数少了。
如果线程池利用率不高,但CPU利用率很高,那很可能是线程池配置的线程数太多了。
3 降低锁竞争的方法
非竞争的同步可以完全在JVM中进行处理,而竞争的同步可能需要操作系统的介入,从而增加开销
有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,以及每次持有该锁的时间。如果二者的乘积很小,那么大多数获取锁的操作都不会发生竞争,因此在该锁上的竞争不会对可伸缩性造成严重的影响。然而,如果在锁上的请求量很高,那么需要获取该锁的线程被阻塞等等待。在极端的情况下,即使仍有大量工作等待完成,处理器也会被闲置。
在保证程序正确性的前提下,解决同步带来的性能损失的第一步不是去除锁,而是降低锁的竞争。通常,有以下三类方法可以降低锁的竞争:减少锁的持有时间,降低锁的粒度,以及采用非独占的锁或非阻塞锁来代替独占锁。
- 减少锁竞争的有效方法是尽可能缩短把持锁的时间。这可以通过把锁无关的代码移出到synchronized块来实现,尤其是那些花费时间长的操作,以前阻塞操作。
- 减少锁的粒度,可以通过分拆锁(一个分成两个,适用于中等竞争强度的锁)和分离锁(一个分成多个,适用于竞争激烈的锁)来实现,这样就会减少对同一锁的调用频度,可伸缩性得以提高,这比使用一个锁来锁住整个对象具有高并发性。
- 减少上下文切换的开销关键在于将阻塞方法的调用放入另一线程中进行调用。
- java程序中串行化首要的来源是独占的资源锁,所以可伸缩性通常可能通过以下这些方法提升:减少用于获取锁的时间、减少锁的粒度、减少锁的占用时间、或者用非独占锁(ReadWriteLock)或非阻塞锁来取代独占锁。
4 避免Synchronization的方法
- 使用ThreadLocal对象
- 使用基于CAS的方式
Monitoring Threads and Locks
JLM(Java Lock Monitor) 查看 Java 中锁使用的情况
How to Monitor JVM Lock Contention, Hot Locks