【JVM】高效并发之锁优化技术

当采用互斥同步方法实现线程安全的时候,线程阻塞将会对系统性能产生很大的影响,因为线程的挂起和恢复操作都要切换到内核态中完成,频繁地切换将会给系统带来性能损耗。
锁优化技术的目的就是尽量减少不必要的线程切换,提高系统的并发处理能力。

下面是一些锁优化技术的介绍:

一、锁消除

有时候在代码中会有一些不必要的同步,这样在编译期就能进行优化,譬如对代码上要求同步,但实际上是不可能发生数据共享的锁进行消除。虚拟机会利用逃逸分析技术去判断一段代码中,堆上的数据会不会逃逸出去被其它线程访问到,然后进行相应的优化。
一方面可能是人为因素添加了不必要的同步措施,另一方面,同步的代码在Java中普遍存在,无意中我们就会用到一些可能自己都不知道的同步方法。
例如以下代码:

public String concatString(String s1,String s2,String s3)
{
    return s1+s2+s3;//这里似乎没有同步,实际上是吗?
}

String是不可变的类,因此对字符串的操作总是通过生成新的对象来进行,以上代码可能会变成下面的样子:

public String concatString(String s1,String s2,String s3)
{
    StringBuffer stringBuffer=new StringBuffer();
    stringBuffer.append(s1);
    stringBuffer.append(s2);
    stringBuffer.append(s3);
    return  stringBuffer.toString();
}

我们看看StringBuffer里append()方法的源码:

/**
 * Adds the specified string to the end of this buffer.
 * <p>
 * If the specified string is {@code null} the string {@code "null"} is
 * appended, otherwise the contents of the specified string is appended.
 *
 * @param string
 * the string to append (may be null).
 * @return this StringBuffer.
 */
 public synchronized StringBuffer append(String string) {
     append0(string);
     return this;
}

我们发现,语义上看似没有同步的代码,很有可能会涉及同步。但不要担心,虚拟机会检测到stringBuffer对象的作用域是在concatString()方法内,其它线程无法访问它,因此是线程安全的,编译器对其进行优化后,这段代码在实际运行中会忽略掉同步处理。

二、 自旋锁与自适应自旋锁

自旋锁技术原理就是让线程执行一个忙循环以等待其它线程释放锁。
自旋锁是基于一种乐观的期待:共享数据的锁定状态时间一般非常短,如果为了这点儿时间挂起线程并不划算,为了避免线程切换,于是自己进行忙循环,稍等那么一点儿说不定就能成功获得锁。

但实际情况有可能没有这么好,自旋会白白消耗CPU资源,如果自旋很久都没有获得锁,再自旋下去将只会造成更大的浪费,因此自旋是有限度的。一般自旋的默认次数是10次。

自适应自旋锁则是在自旋锁的基础上,让虚拟机稍微有一些记忆功能。对于一个锁对象,如果自旋刚刚获得成功,并且持有锁的线程正在运行中,那么虚拟机会认为下一次也有很大的可能会成功,因此适当延长允许自旋等待的时间,比如增加到100个循环。如果对于某个锁,自旋很少成功,就认为下一次也不大可能成功,于是可以取消自旋等待,避免浪费CPU资源。

自旋锁在JDK 1.4.2中就引入了,在JDK 1.6中自旋锁改为默认开启,还引入了自适应自旋锁。

三、 锁粗化

这似乎与我们编写同步代码的原则相悖:我们总希望将代码的同步粒度控制得尽量小,这样如果存在锁竞争,等待锁的线程能在短时间内拿到锁。这对进行自旋等待的线程非常有利。

这里只针对特殊的情况,比如连续对同一个对象反复加锁和解锁,甚至在循环体中加锁和解锁,这样即使没有线程竞争,频繁地进行互斥同步操作也会导致性能损耗。
锁粗化技术能轻易解决这个问题,虚拟机检测到一串零碎的操作都是对同一个对象加锁时,就会把加锁的范围粗化到整个连串操作的外部。例如下面的代码:

for(int i=0;i<10;i++){
    synchronized(count){
       count+=i;
}
}

虚拟机可能会将其粗化成为:

synchronized(count)
{
    for(int i=0;i<10;i++){
       count+=i;
    }
}

四、 轻量级锁

轻量级锁是相对于使用系统互斥量来实现的传统锁(重量级锁)而言,它不能代替重量级锁,它的目的是,在没有多线程竞争的情况下,减少重量级锁使用系统互斥量产生的性能消耗。

理解轻量级锁之前,首先要了解一个依靠一条硬件指令来完成的操作:
比较并交换(Compare-and-Swap)即CAS。在IA64、x86指令集中就有cmpxchg指令完成CAS功能。JDK 1.5之后才能使用该操作。

CAS指令需要三个操作数,分别是变量内存地址、旧的预期值和新值。CAS指令执行时,当且仅当变量内存地址指向的值与旧的预期值相同时,处理器用新值替换变量地址指向的值,否则就不替换。无论有没有替换,最后都会返回变量地址所指向的值,上述过程是一个原子操作。
使用CAS操作来避免阻塞同步的一个简单例子就是AtomicInteger类的incrementAndGet()方法,这个方法是原子的。

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
}

 

接下来将讲述轻量级锁是如何实现的:

在HotSpot虚拟机的对象头中,有一部分数据用于存储对象自身的运行时数据,该部分数据在32位虚拟机占32bit,在64位虚拟机占64bit,其中2bit用于存储锁标志位。官方称它为Mark Word,它是实现轻量级锁和偏向锁的关键。

我们只需要知道,根据Mark Word中的锁标志位就能知道当前对象锁的状态,至于对应什么值,可以不必理会,接下来讲解一下轻量级锁的加锁和解锁过程。

轻量级锁的加锁过程:

代码进入同步块的时候,如果检测锁标志位发现该对象没有被锁定,虚拟机会在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,将目前对象的Mark Word拷贝到Lock Record中,这个在栈帧中的拷贝称作Displaced Mark Word。
接着,虚拟机使用CAS操作尝试将该对象的Mark Word内容更新为指向Lock Record的指针,如果成功,那么该线程就获得了该对象的锁,并且对象的Mark Word中2bit的锁标志位转变为轻量级锁状态值。

如果这个更新失败了,有可能是线程已经拥有该对象的锁,只需要判断该对象的Mark Word是否指向当前线程的Lock Record,如果是,则直接进入同步块执行。如果判断该对象的Mark Word不是指向当前线程的Lock Record,说明该对象被其它线程抢占了,这时发生了竞争,轻量级锁将会失效,要膨胀为重量级锁,于是将对象Mark Word中的锁标志位设置为重量级锁状态,Mark Word存储的指针指向重量级锁的互斥量。这样接下来访问该对象的线程发现此对象是重量级锁状态,于是进入阻塞状态。

从上面加锁过程可以看出,如果没有线程竞争,对象锁状态一直是轻量级锁,不会变成重量级锁,这样就能避免传统的重量级锁使用操作系统互斥量产生的性能损耗。但如果存在竞争,轻量级锁最终还是要膨胀为重量级锁,反而比纯粹的重量级锁消耗了更多资源。

轻量级锁的解锁过程:

解锁的时候,如果对象的Mark Word指向的该线程的Lock Record,就会用CAS操作把对象的Mark Word和该线程中的Displaced Mark Word替换回来,如果替换成功,则同步过程完成了。如果替换失败,说明有其它线程尝试过获取该锁,则在释放锁的同时也要唤醒挂起的线程。

五、偏向锁

偏向锁比轻量级锁更懒惰,能在无竞争的情况下消除整个同步,CAS操作只进行一次。

若虚拟机启用了偏向锁,当线程获取对象的时候,查看对象锁标志位发现对象未被锁定,就会将对象的锁标志位改为偏向锁状态,接着使用CAS操作把取到该对象偏向锁的线程ID记录在对象的Mark Word中,如果记录成功,这条线程就拥有了该对象的偏向锁,以后该线程每次进入该对象的同步块时,虚拟机都不再进行任何同步操作。如果记录失败,说明存在锁竞争,偏向模式就要结束,对象恢复到轻量级锁状态,接下来按照轻量级锁的步骤执行。

参考资料:
《深入理解Java虚拟机》机械工业出版社 周志明 2013
【2016-4-6 20:56:12】

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注