参考资料:《实战Java高并发程序设计》
1.锁优化的几个方面
1.减少锁持有时间
- 减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。
2.减小锁粒度
- 减少锁粒度,是指缩小锁定对象的范围,从而减少锁冲突的可能性,进而提升系统的并发能力。
- 但要注意,减少锁粒度会引入一个新的问题,即:当系统需要取得全局锁时,其消耗的资源会比较多。所以只有当获取全局信息的方法调用并不频繁时,这种减少锁粒度的方法才能真正意义上提供系统吞吐量。
3.读写分离锁来替换独占锁
- 使用读写分离锁来替换独占锁是减少锁粒度的一种特殊情况。
- 如果说减少锁粒度是通过分割数据结构实现的,那么读写锁则是对系统功能点的分割。
- 因读操作本身不会影响数据的完整性和一致性,所以读操作本就该是被准许同时执行的。
- 在读多写少的场合,使用读写锁可以提升系统的并发能力。
4.锁分离
- 将读写锁的思想做进一步的延伸,就是锁分离。读写锁根据根据读写操作功能的不同,进行了有效的锁分离。依据应用的功能特点,使用类似的分离思想,也可以对独占锁进行分离。
- 例如LinkedBlockingQueue,就是通过takeLock和putLock两把锁,实现了取数据和写数据的分离,使两者在真正意义上成为并发的操作。
5.锁粗化
- 通常,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短。但凡事有个度,如果对同一个锁不停的进行 请求、同步和释放,其本身也会消耗宝贵的资源。反而不利于性能优化。
- 因此,虚拟机在遇到一连串连续的对同一锁的请求和释放操作时,便会把所有锁操作整合成对锁的一次请求,从而减少对锁的请求次数,这个操作叫做锁的粗化。
- 例如:
for(int i=0;i<CIRCLE;i++){
synchronized(lock){
}
}
- 就应该改成只在最外层请求一次锁:
synchronized(lock){
for(int i=0;i<CIRCLE;i++){
}
}
- 性能的优化是根据运行时的真实情况对各个资源点进行权衡折中的过程。锁粗化的思想和减少锁持有时间是相反的,需要根据实际情况进行权衡。
2.JVM内部的锁优化策略
1.锁偏向
- 锁偏向是一种针对加锁操作的优化手段。它的思想是:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能。
- 因此,对于几乎没有锁竞争的场合,偏向锁有较好的优化效果。而对于锁竞争比较激烈的场合,则效果不佳。
- 使用Java虚拟机参数-XX:+UseBiasedLocking可以开启偏向锁。
2.轻量级锁
- 如果偏向锁失败,虚拟机并不会立即挂起线程。它会使用一种称为轻量级锁的优化手段。
- 轻量级锁只是简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。
- 如果线程获得轻量级锁成功,则可以顺利进入临界区。否则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。
3.自旋锁
- 锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,还会做最后的努力——自旋锁。
- 由于当前线程暂时无法获得锁,但什么时候可以获得还是未知数,也许在几个CPU时钟周期后,就可以获得,那简单粗暴地挂起线程可能就是得不偿失的。因此,系统会进行一次赌注:它会假设在不久的将来,线程可以得到这把锁。因此虚拟机会让当前线程做几个空循环(即 自旋),在经过若干次自旋后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真实地将线程在操作系统层面挂起。
4.锁消除
- 锁消除是指JVM在进行JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。
- 一个锁消除发生的场景比如:对一个方法体内的局部变量,使用了线程安全的类型(例如StringBuffer、Vector)。如果虚拟机检测到这种情况,就会将这些无用的锁操作去除。
- 锁消除涉及的一项关键技术为逃逸分析。所谓逃逸分析,是指观察某一个变量是否会逃出某个作用域。
- 逃逸分析必须在-server模式下进行,可以使用-XX:+DoEscapeAnalysis参数打开逃逸分析。使用-XX:+EliminateLocks参数可以打开锁消除。
end