同步和对象锁-Synchronization and Object Locking

    Java语言的一个主要强项就是对多线程程序的内置支持。一个对象在多个线程之间共享读取时可以通过加锁来实现有序同步的访问。Java提供原语来指明关键代码区域,原语作用于共享对象,并保证同一时刻只有一个线程能执行这些代码。第一个进入代码区域的线程锁定共享对象,当与这个共享对象相关的第二个线程要进入相同的代码区域时,它只能等待,直到第一个线程解锁这个共享对象。
    在Java官方的HotSpot虚拟机中,每一个对象前面部分都是由对象头(HeaderWord)、对Class的指针组成。对象头中保存了HashCode以及分代垃圾收集的标识位。另外,对象头还可以用来实现轻量级锁。下图显示了对象头的组成和不同状态下的表现形式。

    图的右边部分说明了标准加锁流程。一般来说,一个未被加锁的对象的HeaderWord的最后两位的值是01。当对象的某个方法同步执行时,当前线程的当前栈帧中会创建一个锁记录(LockRecord),锁记录包括从HeaderWord复制而来的内容,以及一个对该对象的引用,如下图:

【改天画图,睡觉去了】

然后JVM会尝试使用CAS操作修改对象的HeaderWord,使HeaderWord指向栈帧中的锁记录。如果修改成功,则该线程随即拥有锁。HeaderWord的最后两位是00,将对象标识为已加锁,剩余前面的bit都用于表示锁记录。
    如果CAS操作因为对象已被锁定而失败,JVM首先检查HeaderWord是否指向当前线程的方法栈。如果是,说明当前线程已经拥有该对象的锁并且可以安全地继续执行。对于一个被递归锁定的对象,栈帧中的锁记录置为0而不是HeaderWord。如果两个线程并发地竞争同一个锁对象,轻量级锁应该膨胀为重量级锁,以便管理这些等待的线程。
    尽管轻量级锁的代价比重量级锁要小得多,但CAS操作都是在多核CPU的计算机上自动执行的,所以轻量级锁的性能也可能会变差,即使大多数对象只是被某一个线程加锁和解锁。在Java 6中,这个缺点由通过所谓的‘自由存储偏向锁技术’[RuSele06]得以解决,它使用类似于[KaajiaYa02]的概念。第一次加锁会执行CAS操作并在HeaderWord中设置线程ID,可以看做该对象偏向这个线程,接下来该线程的加锁和解锁不需要原子操作或设置任何HeaderWord。该线程的栈帧中也不会有锁记录,因为一个已偏向的对象不需要检查锁记录。
    当一个线程竞争一个已经偏向其它线程的锁对象时,会撤销偏向锁,使该对象看起来像是正常(公平)地实现了加锁。具体做法是,遍历被偏向的线程的栈,根据轻量级锁的方案调整该对象的锁记录,对象的HeaderWord会指向最早的锁记录,这时所有的线程都要挂起。另外,当对象的HashCode生成后,偏向锁也必须要撤销,线程ID的位置改为存储HashCode,因为在HeaderWord中两者共用同一块存储区域。
    按照多线程共享的用途设计的对象(比如生产者/消费者队列),不适合使用偏向锁。因此,当一个类的实例频繁地销毁,那么该类的偏向锁会被禁用,这被称为‘批量销毁’。如果在一个被禁用了偏向锁的类实例上调用加锁的代码,将执行标准的轻量级锁,新创建的实例都会标记为‘不可偏向的’。
    还存在一个叫做‘批量重新偏向’的相似机制,是针对类的对象被不同的线程加锁/解锁但没有并发的场景的优化。它在没有禁用偏向锁的情况下使类所有实例的偏向锁都失效。类中的epoch值像时间戳一样来标记偏向锁的有效性。在对象创建时把该值复制到HeaderWord中,相应类中的epoch值以递增的形式高效地实现批量重新偏向。下一次对此类的对象加锁时,代码在头字中检测到不同的值,会将对象重新偏向到当前线程。

    本文翻译自https://wiki.openjdk.java.net/display/HotSpot/Synchronization ,by@六吨代码

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章