java很多threaddump中,都可以看到thin lock, fat lock, spin lock,这些lock都与java语言、os有密切的关系。
回到一个简单的问题,在java中,如何实现synchronizd?
最简单的一种做法是,利用os的mutex机制,把java的同步(基于object),翻译成os相关的monitor_enter和monitor_exit原语。
回到java锁本身,锁在不同的应用下有着不同的统计表现,而大部分的统计数据表明,其实线程抢锁,即锁竞争,都是短暂的,在大部分的情况下,几乎都不会发生锁竞争的现象。
也就是说,java锁,从安全性的角度来看,是有点累赘。
因此,大量的专家都在锁上针对这样的统计特性对java锁进行优化。
其中一种优化方案是,我们对所有的锁都需要monitor_enter和monitor_exit吗?事实上不需要。
如果我们把monitor_enter/monitor_exit看成是fat lock方式,则可以把thin lock看成是一种基于cas(compare and swap)的简易实现。
这两种锁,简单一点理解,就是:
而基于cas方式的实现,线程进入竞争状态的,获得锁的线程,会让其他线程处于自旋状态(也称之为spin mode,即自旋),这是一种while(lock_release) dostuff()的busy-wait方式,是一种耗cpu的方式;而fat lock方式下,一个线程获得锁的时候,其他线程可以先sleep,等锁释放后,再唤醒(notify)。
cas的优点是快,如果没有线程竞争的情况下,因为cas只需要一个指令便获得锁,所以称之为thin lock,缺点也是很明显的,即如果频繁发生线程竞争,cas是低效,主要表现为,排斥在锁之外的线程是busy wait状态;而monitor_enter/monitor_exit/monitor_notify方式,则是重量级的,在线程产生竞争的时候,fat lock在os mutex方式下,可以实现no busy-wait。
于是,jvm早期版本的做法是,如果t1, t2,t3,t4...产生线程竞争,则t1通过cas获得锁(此时是thin lock方式),如果t1在cas期间获得锁,则t2,t3进入spin状态直到t1释放锁;而第二个获得锁的线程,比如t2,会将锁升级(inflation)为fat lock,于是,以后尝试获得锁的线程都使用mutex方式获得锁。
这种设计为锁提供了两条路径:thin lock路径和fat lock路径,大部分情况下,可能都是走thin lock路径,而可能少部分情况,是走fat lock路径,这种方式提供了锁升级,但是避免不了busy wait,而且thin-lock升级fat-lock之后,没有办法回退到thin-lock(性能比fat-lock更好)。
tasuki锁为这种方式做了2个优化:
1) 避免cas导致busy wait
2) fat lock可以deflate(与inflate刚好相反)为thin lock(之前是thin lock变成fat lock之后便不能再回退)。
经过这样的改造后,锁性能提高了10%以上。
目前,oracle的bea jrockit与ibm的jvm都实现了tasuki锁机制,唯一的不同是,在锁实现上都做了不同启发式的设计,即根据运行时采样的数据,动态调整一些权值数据,一边左右lock inflation/lock defaltion的过程(一颗树的两个分支),获取更好的锁性能。
对jvm锁有兴趣的朋友可以与我联系(mail:)
另外一种针对锁的优化手段,叫做lazy-unlocking,可以在tasuki lock之上构造,可以参见我另外一篇随笔