Java SE的每个版本都花费了大量的心思在同步性能优化上,Java SE 6也不例外。当多个线程需要同时访问共享的可变数据时,需要使用锁来同步多个线程的访问。根据竞争程度的不同,锁又可分为竞争性锁(contended lock)和非竞争性锁(uncontended lock)。由于大多数的锁都是非竞争性的,Java SE主要将精力用于优化非竞争性锁的性能,同时Java SE 6也优化了竞争性锁的性能。对竞争性锁优化的主要方法是自适应自旋(adaptive spinning),对非竞争性锁优化的方法有偏向性锁(Biased Locking)、锁粗化(Lock Coarsening)和锁取消(Lock Elision)。下面就一一谈下这四种优化同步性能的方法。
自适应自旋(adaptive spinning)
当两个线程竞争同一个锁时,其中一个线程获得锁,而另一个线程被阻塞直到锁被释放。通常的做法是操作系统悬挂起后一个线程,当前一个线程释放锁时唤醒后一个线程。这存在线程上下文切换的开销,另一种方法是后一个线程忙等待一段时间(通常就是几个时钟周期),这也就是自旋(spinning),当线程等待锁的时间极短时,这种方法比悬挂方法更有效率。Java SE 6使用自适应(adaptive)的技术进一步提高性能,它的主要思想是动态监控自旋成功/失败的比率,如果该几率比较大就使用自旋方法,否则就使用悬挂方法。自适应自旋技术主要是对多核系有效,它能避免使用悬挂方法所造成的线程上下文切换开销。
偏向性锁(Biased Locking)
偏向性锁主要基于这样的一个观察,一个锁在它的生命周期中通常只被一个线程所拥有。如果线程1拥有锁,这个锁就偏向(biase toward)于线程1,下次线程1需要再次获得锁时它就具有优先权。
注:偏向性锁对性能优化的原理我还并不是十分清楚,官方文章说偏向性锁能够避免一些原子操作,而这些原子操作十分耗时,但是我不明白为什么需要这些原子操作。这里的原子操作主要指的是CAS(Compare and Swap)。
锁粗化(Lock Coarsening)
当线程需要获得-释放锁,然后再获得-释放同一个锁,且第一次释放锁和第二交获得锁之间没有其它操作时,可以将两次获得-释放合并成一个获得-释放锁,这可以减少获得-释放锁的开销。例如,对下面的代码就可以使用锁粗化技术。
void addThing(StringBuffer buf) { buf.append("thing 1"); buf.append("thing 2"); }
锁粗化带来的一个不好的效果就是线程占用锁的时间变长,这导致其他线程等待更长的时间。基于这种原因,锁粗化不能用于循环上。例如对于下面的代码就不能使用锁技术。
void addRepeatedThing(StringBuffer buf, int count) { for (int i = 0; i < count; i++) { buf.append("thing"); } }
锁取消(Lock Elision)
有时我们可以通过逸出分析(escape analysis)确定这个锁只能在一个线程使用,这时锁不起任何作用,可以完全取消这个锁。比如下面的锁就可以完全去掉,因为new Object()只能在一个线程中使用,别的线程不能访问到这个锁。
synchronized (new Object()) { doSomeThing(); }
上面是个伪造的例子,下面是个更加实际的例子。在这个例子中,StringBuffer对象sb是个局部变量,只能被当前线程所访问,因此对sb的所有操作都可以安全地取消掉同步,通过这种方法StringBuffer可以达到和StringBuilder几乎一样的性能。
public String toString() { StringBuffer sb = new StringBuffer(); sb.append("field1: ").append(field1); sb.append(", field2: ").append(field2); return sb.toString(); }