高效并发是jdk 5到jdk6的一个重要改进,实现了各种锁优化技术
1 自旋锁与自适应自旋:挂起线程和恢复线程的操作都需要转入内核状态中完成,这些操作给虚拟机并发的性能带来了很大的压力,在很多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。为了让线程等待,只需让线程执行一个自旋,这就是所谓的自旋锁。
自旋等待虽然避免了线程切换的开销,但它要占用处理器时间的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有价值的工作,这就会带来性能的消耗,因此自旋锁等待的时间必须要有限度,如果自旋的次数超过了一定的限度还没有成功获取锁,就应当用传统的方式去挂起锁,默认自旋十次。
在jdk 6中对自旋锁进行了优化,引入了自适应的自旋,自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间;另一方面,如果对于某个锁,自旋很少成功获得过锁,那么以后可能直接忽略掉自旋过程,以避免浪费处理器资源
2 锁消除:锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把他们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无需进行。
public String concatString(String s1,String s2){
return s1+s2;
}
String是一个不可变的类,对于字符串的连接操作总是通过生成新的String对象来进行的,因此javac编译器会对String连接做自动优化,会转变成StringBulider对象的append操作,javac转化后的字符串连接操作
public String concatString(String s1,String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
每个append方法中都有一个同步块,锁的对象就是sb,虚拟机观察变量sb,经过逃逸分析后发现它的动态作用域被限制在concatString方法内部,也就是sb的所有引用都永远不会逃逸到
concatString方法外,其他线程无法访问到它所以这里虽然有锁,但是可以安全的消除掉。在解释执行时,这里仍然会加锁,但是经过服务端编译器的即时编译之后,这段代码就会忽略所有的同步措施而直接执行。
3 锁粗化:原则上,我们在编写代码的时候,总是推荐同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即时存在锁竞争,等待锁的线程也能尽快的拿到锁。大多数情况下,上面的原则是正确的,但如果一系列的连续操作都是对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即时没有线程竞争,频繁进行互斥同步操作也会导致不必要的性能损耗,上面的代码append方法就是这类情况,如果虚拟机探测到这类操作,就会把加锁不同的范围扩展到整个操作序列的外部,即扩展到第一个append操作前直到最后一个append操作后,这样只需要加一次锁了。
4 轻量级锁:轻量级锁是jdk 6新加入的新型锁机制,要理解轻量级锁,必须要对hotspot虚拟机对象的内存布局有所了解。对象的对象头里会存储对应的锁标志位;在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(rock record)的空间,用于存储锁对象目前的Mark word的拷贝,即Displaced Mark Word,然后虚拟机使用CAS操作尝试把对象的Mark Word更新为指向rock record的指针,如果这个更新成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位将转变为“00”,表示此对象处于轻量级锁定状态,如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块进行操作;否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程竞争同一个锁的情况,那轻量级锁就不再有效,必须要升级为重量级锁,锁标志的状态值为“10”,后面等待的线程也必须进入堵塞状态。轻量级锁的解锁过程也是通过CAS操作来执行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果成功替换,那整个同步过程顺利完成;如果替换失败,说明有其他线程尝试过获取该锁,就需要在释放锁的同时,唤醒被挂起的线程。
轻量级锁提升程序同步性能的依据是对于绝大部分的锁,在整个同步期内不存在竞争这一经验法则;如果没有竞争,轻量级锁通过CAS操作成功避免使用互斥量的开销,如果存在竞争,除了互斥量的开销外,还额外发生了CAS的操作开销,反而比传统的重量级锁更慢。
5 偏向锁:也是jdk 6引入的,它的目的是消除数据在无竞争情况下的同步,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下消除同步,连CAS都不用。
如果当前虚拟机启用了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机会把对象头中的标志为设置为“01”,把偏向模式设置为“1”,表示进入偏向模式,同时把获取到这个锁的线程id记录到对象的Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁的同步块时,虚拟机可以不再进行任何同步操作,一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式马上结束。根据目前是否处于被锁定的状态决定是否撤销偏向,撤销后标志为恢复到未锁定“01”或者轻量级锁定(“00”)的状态。
当对象进入偏向锁状态是,Mark Word大部分空间都用于存储持有锁的线程id了,这部分空间暂用了原来存储对象哈希码的位置,那原来哈希码怎么办呢?在java程序里一个对象如果计算过哈希码,就应该一直保持不变(推荐但不强制,用户可以重载hashacode方法按照自己的意愿返回哈希码),否则很多依赖对象哈希码的api可能存在出差风险,而绝大多数对象哈希码的来源object:hashcode方法,返回的是对象的一致性哈希码,这个值能强制不变的原因是通过对象头存储的计算结果,再次调用不会改变。因此当一个对象计算过一致性哈希码后,它就再也无法进入偏向锁了,而当一个对象目前处于偏向锁,又收到其一致性哈希码的请求时,它的偏向状态会立即撤销,并且锁会膨胀到重量级锁。