文章目录
一、提高锁性能的建议
1.1 减小锁的持有时间
- 尽量在关键代码部分持有锁,防止在持锁过程中执行大量非关键代码。
- 即尽可能减少某个锁的占有时间,以减少线程间互斥的可能性,进而提升系统的并发能力。
public synchronized void syncMethod(){
otherCode1(); // 无需同步的代码
mutexMethod(); // 需同步的代码
otherCode2(); // 无需同步的代码
}
在这段代码中,只有 mutexMethod() 需要同步,而 otherCode1() 和 otherCode2() 不需要同步。然而我们对整个方法进行了同步,在执行无需同步的代码时需要花费一些时间,导致持锁时间变长,锁互斥的可能性增大。
我们对其改进一下:
public void synMethod(){
otherCode1(); // 无需同步的代码
synchronized(this){ // 同步代码块
mutexMethod();
}
otherCode2(); // 无需同步的代码
}
在改进后的代码中,我们只对需要同步的代码进行同步,减少了持锁时间,一定程度降低了互斥可能性,提升了并发量。
1.2 减小锁粒度
- 定义:通过缩小对象的范围,从而减少锁的冲突。
- HashMap 有两个重要的方法 get() 和 put(),但它是不安全的,我们可以对整个 HashMap 加锁以此来获得一个线程安全的对象,但是加锁的粒度太大。
- 对于 ConcurrentHashMap,它内部被分为了很多的小段(默认16个),如果需要在 ConcurrentHashMap 中新增一个数据,不需要将整个Map加锁,只需将相应的段加锁即可。在多线程环境中,如果多个线程同时进行 put() 操作,只要被加入的数据不在同一个段中,则线程间可以做到真正的并行。
- 问题:当需要取得全局锁时,其消耗的资源会比较多。以 ConcurrentHashMap 为例,它的 size() 方法,返回 map 中的 K-V 数量,此时需要获取所有子段的锁
1.3 读写锁替换独占锁
- 使用读写分离锁替代独占锁是减小锁粒度的一种特殊情况。
- ConcurrentHashMap 是通过分割数据结构实现的,而读写锁则是对系统功能点的分割。
- 读写锁适合读多写少的场合。
1.4 锁分离
依据程序的功能特点,使用类似分离的思想,对独占锁进行分离。
例如 LinkedBlockingQueue 的实现,其 take() 和 put() 函数功能分别是从队列中取数据和向队列中加数据,两个操作分别作用于队列的头和尾。如果使用独占锁,take() 和 put() 操作无法并发;在 JDK 中,使用了锁分离,将 take() 和 put() 操作分离
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
takeLock 和 putLock 分别在 take() 和 put() 操作中使用,因此 take() 和 put() 操作就不存在竞争关系了,可以相互独立。
1.5 锁粗化
- 一般情况下,为了保证多线程间的有效并发,要求每个线程持有锁的时间尽量短;但如果频繁对同一个锁不停地进行请求、同步和释放,也会消耗系统资源,反而不利于性能。
- 概念:所以虚拟机在遇到一连串连续地对同一锁不断进行请求和释放的操作时,会把所有锁操作整合成对锁的一次请求,减少对锁的请求同步次数。
public void demoMethod(){
synchronized(lock){
// do sth
}
// 做其他无需同步,且执行时间短的代码
synchronized(lock){
// do sth
}
}
会被整合如下:
public void demoMethod(){
synchronized(lock){
// do sth
// 做其他无需同步,且执行时间短的代码
// do sth
}
}
在平时开发中,也可以有意识地进行锁粗化操作。
例如:
for(int i=0;i<number;i++){
synchronized(lock){}
}
改写为如下所示,此时只会在最外层请求一次锁
synchronized(lock){
for(int i=0;i<number;i++){
}
}
二、JVM中的锁优化
2.1 锁偏向
锁偏向是一种针对加锁操作的优化手段。
核心思想
如果一个线程获得了锁,那么锁就进入了偏向模式。当这个线程再次请求锁时,无需做任何同步操作。所以这样就节省了锁申请操作,从而提升程序的性能。
适合场景
几乎没有锁竞争的场合,因为多次请求可能是一个线程发出的;对于锁竞争激烈的场合,效果不佳。
2.2 轻量级锁
偏向锁失败,会升级为轻量级锁。
过程
将对象头部作为指针,指向持有锁的线程堆栈的内部,通过这个方法来判断线程是否持有对象锁。
如果线程获取轻量级锁成功,就可以进入临界区;如果失败,表示其它线程先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁
2.3 自旋锁
- 线程的挂起和唤醒是需要消耗系统资源的;有时一些线程处理任务的时间其实很短,若将其挂起反而浪费时间和资源。
- 假设在不久的将来,线程可以得到这把锁。因此,虚拟机会让当前线程做几个空循环,空循环之后若可以得到锁那么就顺利进入临界区;否则将被挂起
2.4 锁消除
- Java虚拟机在进行JIT编译时,会进行上下文扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间
- 例如在不存在并发竞争的场合中使用了Vector、StringBuffer等,此时由于不存在并发竞争,所以相关的锁是没有必要的。如果虚拟机检查到这种情况,可以将这些无用的锁操作去除
锁消除涉及的一项技术名为“逃逸分析”,即观察某一个变量是否会逃出某一个作用域
- 逃逸分析必须在-server模式下进行
- -XX:_DoEscapeAnalysis:打开逃逸分析
- -XX:+EliminateLocks:打开锁消除