Java进阶(一)提高锁性能

java concurrent programming

Posted by Kinsomy on January 18, 2019

1 提高锁性能

1.1 减小锁持有时间

锁持有的时间会影响其他线程的等待时间,从而影响整体性能,如果一个锁持有时间较长,在并发量大的情况下,等待线程数量会大幅增加,导致竞争激烈,系统性能低下。

解决方案:分析代码,在必要时进行同步,减小同步范围,也就是只在互斥代码部分才持有锁,减少锁持有时间,提高系统吞吐性能。

看个例子:

public synchronized void syncMethod(){
    othercode1();
    mutextMethod();
    othercode2();
}

上面是一个同步方法,在这个方法上用了synchronized关键字加锁,方法里面执行三个操作,只有mutextMethod();是互斥的操作,也就是必须同步执行,其他两个方法可以异步执行,如果 othercode1()和othercode2()比较耗时的话,那这个方法持有锁的时间就会变长,外部就需要一直等待。

优化方法就是分析上面的方法真正需要进行同步的部分是哪里,不要全部加锁,而是局部加锁,减小锁持有时间,很显然,只要在执行到mutextMethod();这一行的时候持有锁就行了,因此改成下面代码。

public void syncMethod(){
    othercode1();
    synchronized(this){
        mutextMethod();
    }
    othercode2();
}

JDK源码里也使用了这个方法,看一个Pattern的案例:

public Matcher matcher(CharSequence var1) {
    if (!this.compiled) {
        synchronized(this) {
            if (!this.compiled) {
                this.compile();
            }
        }
    }
    Matcher var2 = new Matcher(this, var1);
    return var2;
}

减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力

1.2 减小锁粒度

减小锁粒度典型使用场景:ConcurrentHashMap

因为HashMap不是线程安全的,如果想要变得线程安全,最简单的做法就是对整个HashMap加锁,但是这样显然锁的粒度太大。

JDK7中ConcurrentHashMap内部细分了若干个小HashMap,叫做段(SEGMENT),默认16个。

在ConcurrentHashMap中,如果要进行put操作,会先根据需要put的hashcode得到该item应该被加到哪个SEGMENT中去,然后再对那个SEGMENT加锁,而不是对整个ConcurrentHashMap加锁,这样的话,如果一个ConcurrentHashMap对象有多个item需要执行put操作,而恰巧这几个item被加到不同的SEGMENT中,那就完全可以并行操作,因为默认有16个SEGMENT,因此默认情况下,最多可以并行16个put操作。

JDK8中则不再采用SEGMENT的方案,而是用了CAS算法。

1.3 读写分离锁替换独占锁

读写锁也是一种局部加锁,在功能上做了拆分,对于读操作,不会对数据做修改,可以并发执行。

读多写少的场合,使用读写锁可以有效提升系统的并发能力。

1.4 锁分离

典型案例:LinkedBlockingQueue 这是一个链表实现的队列,既然是队列数据结构,那就是遵循FIFO先进先出,take()put()是读取和添加操作,读取作用在链表的头部,添加是在尾部,两者没有数据冲突,那么使用独占锁的话,每次进行takeput操作都会获得队列的锁,导致同一时间只有一个操作可以执行,其他都要等待,影响了并发性能,所以在实现中没有使用独占锁,而是使用分离锁。take和put分别持有一把锁。

//take()持有takeLock
private final ReentrantLoc takeLock;
private final Condition notEmpty;
//put()持有putLock
private final ReentrantLock putLock;
private final Condition notFull;

//构造方法
public LinkedBlockingQueue(int var1) {
    this.count = new AtomicInteger(;
    this.takeLock = newReentrantLock();
    this.notEmpty =this.takeLock.newCondition();
    this.putLock = new ReentrantLoc();
    this.notFull =this.putLock.newCondition();
    if (var1 <= 0) {
        throw newIllegalArgumentException();
    } else {
        this.capacity = var1;
        this.last = this.head = newLinkedBlockingQueue.Node(Object)null);
    }
}

接下来分析一下take()的源码:

    public E take() throws InterruptedException {
        boolean var2 = true;
        AtomicInteger var3 = this.count;
        ReentrantLock var4 = this.takeLock;
        var4.lockInterruptibly();

        Object var1;
        int var8;
        try {
            //队列为空时持续等待
            while(var3.get() == 0) {
                this.notEmpty.await();
            }

            //队列不为空,获取数据
            var1 = this.dequeue();
            //队列数量减一,var8是减一前的值
            var8 = var3.getAndDecrement();
            //如果之前队列数量大于1,通知其他take操作
            if (var8 > 1) {
                this.notEmpty.signal();
            }
        } finally {
            //释放锁
            var4.unlock();
        }
        //有剩余空间,通知put操作可以添加
        if (var8 == this.capacity) {
            this.signalNotFull();
        }

        return var1;
    }

put()源码类似,不做记录。

1.5 锁粗化

虚拟机对连续对同一锁进行请求和释放的操作,会把所有的锁操作合并成对锁的一次请求,从而减少对锁的请求同步次数,这种操作就是锁粗化。

for (int i = 0; i < count; i++){
	synchronized (lock){
		//do sth
	}
}

//锁粗化
synchronized (lock){
for (int i = 0; i < count; i++){
		//do sth
	}
}

上面代码第一个循环每次都要请求锁完成一些操作,这样多次的请求释放同样消耗性能,比较合理的做法就是做一次锁粗化,在循环外层请求一次锁。

锁粗化和减少锁的持有时间是相反的操作,但最终想要达到的目的是相同的,就是为了提高并发的性能,至于如何选择,要具体情况具体分析。

2 JVM对锁的优化

2.1 锁偏向

核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作。

锁偏向适合用在锁竞争不是很激烈的情况,会有比较好的优化效果,设置JVM参数-XX:+UseBiasedLocking可以开启偏向锁。

2.2 轻量级锁

锁偏向失败,就会将线程升级为轻量级锁,具体加锁过程参见文章Java并发编程:Synchronized底层优化(偏向锁、轻量级锁), 如果加锁失败则会变为重量级锁。

2.3 自旋锁

当前线程不挂起,而是做空循环,希望在一段时间之后获得锁进入临界区,如果自旋锁阶段过后还不能得到锁,线程就会被挂起。

2.4 锁消除

JVM在JIT编译时,通过对上下文扫描,去除掉不存在竞争的锁请求代码,减少无用的锁请求时间。

举个例子: 在一个单线程方法里使用Vector去做数据集合的操作,然后我们都知道Vector是线程安全的,里面有很多关于锁的操作,然后单线程同步方法里根本不用关心竞争问题,这时候就会做很多无谓的锁请求,JVM会对其进行优化。

锁消除的关键技术:逃逸分析,就是分析变量是否会越过作用域,被作用域外使用。

“逃逸分析必须在-server模式下进行,可以使用-XX:+DoEscapeAnalysis参数打开逃逸分析。使用-XX:+EliminateLocks参数可以打开锁消除。”

摘录来自: 葛一鸣/郭超/. “实战Java高并发程序设计。” iBooks.

参考资料