问题

  • 某实例未执行过锁代码块,该实例的标识位是101还是001?
  • 轻量级锁和重量级锁执行完成锁代码块后的标识位分别是什么?
  • 轻量级锁执行完锁代码块后,出现其他线程竞争锁,该线程竞争成功后是什么锁?
  • synchronized是否可降级?

java对象Mark Word

32位Header Mark Word

2819184-75508373cf24f69b

即32bit,4个字节

64位Header Mark Word

|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |
|------------------------------------------------------------------------------|--------------------|

即64bit,8个字节

Jol

Jol可用于查看java object内存情况。

实现

monitorenter&monitorexit

当通过synchronized (this)对某个对象加锁时,在java的字节码中是通过monitorenter&monitorexit实现的

ACC_SYNCHRONIZED

当对某个方法标注synchronized时,在java字节码中是通过ACC_SYNCHRONIZED实现的,值得注意的是对static方法和非static方法标注synchronized时,锁的对象是不同的,static方法锁的对象为this.class,非static方法锁的对象为this

锁的类型

无锁

com.kevin.header.ObjectHeaderTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 0f 8a 82 (00000001 00001111 10001010 10000010) (-2104881407)
      4     4        (object header)                           75 00 00 00 (01110101 00000000 00000000 00000000) (117)
      8     4        (object header)                           9f c0 00 f8 (10011111 11000000 00000000 11111000) (-134168417)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
  • 其中00000000 00000000 00000000 01110101 10000010 10001010 00001111可计算出hashcode值
  • 00000001中的001表示当前为无锁状态
  • 偏向锁开启时,BiasedLockingStartupDelay时间后变为偏向锁,即为101

偏向锁

com.kevin.lock.sychronized.BiasedLockApp object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 08 00 19 (00000101 00001000 00000000 00011001) (419432453)
      4     4        (object header)                           da 7f 00 00 (11011010 01111111 00000000 00000000) (32730)
      8     4        (object header)                           9f c0 00 f8 (10011111 11000000 00000000 11111000) (-134168417)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

jstack命令

"main" #1 prio=5 os_prio=31 tid=0x00007fda19000800 nid=0x1c03 waiting on condition [0x0000700007435000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at com.kevin.lock.sychronized.BiasedLockApp.main(BiasedLockApp.java:17)
  • 00000101中的101表示当前已开启偏向锁,可通过通过-XX:+UseBiasedLocking手动关闭
  • jvm默认开启偏向锁,但是默认延迟5s启动,可通过以下两处方式
    • 在执行测试程序前休眠10s
    • 设置-XX:BiasedLockingStartupDelay为0
  • 上述header头表示该对象偏斜的线程id为00000000 00000000 01111111 11011010 00011001 00000000 000010即为00 00 7f da 19 00 08 00,通过jstack可发现main方法所在的线程id为tid=0x00007fda19000800
  • nid为linux线程id 16进制表示形式
  • 开启偏向锁时,标示位一直为101,如果未执行过锁代码段,threadid为0,如果执行过则为执行过的threaid
  • epoch值表示该class对应的实例偏向锁批量重偏向次数
    • 当class对应的实例偏向锁撤销次数达到BiasedLockingBulkRebiasThreshold时,则触发批量重偏向,BiasedLockingBulkRebiasThreshold的默认值为20
    • 批量重偏向会遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁(即该class对应的正在执行锁代码块的实例),将其epoch字段改为新值
    • 如果批量重偏向时,某些线程未处于加锁状态,则在下次执行代码块时,如果threadid一致,则将epoch设置为最新的epoch值,如果threadid不一致,则修改epoch为最新值,并且将threadid偏向当前线程
    • 该class对应的实例偏向锁撤锁次数大于BiasedLockingBulkRevokeThreshold,该class对应的实例关闭偏向锁,BiasedLockingBulkRevokeThreshold的默认值为40
  • 偏向锁也会创建lockrecord,并且重入时会创建多个lockrecord

轻量级锁

com.kevin.lock.sychronized.LockObject object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           88 38 fd 0c (10001000 00111000 11111101 00001100) (217921672)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
  • 升级为轻量级锁的原因
    • 服务才启动,偏向锁未生效
    • 该实例偏向锁出现竞争,升级为轻量级锁
    • 该class对应的实例撤锁次数大于BiasedLockingBulkRevokeThreshold,该class对应的实例关闭偏向锁
  • 偏向锁时,如果当前未执行锁方法,标识位仍为101,但是轻量级锁时,如果未执行锁方法,标识位为001,表示无锁状态,并且锁记录指针均为0
  • 当执行完锁代码时,锁标识位调整为01无锁状态
  • lockrecord中保存了object mark word信息
  • 重入时会创建多个lockrecord

重量级锁

0     4                                             (object header)                           6a 43 00 0a (01101010 01000011 00000000 00001010) (167789418)
4     4                                             (object header)                           9a 7f 00 00 (10011010 01111111 00000000 00000000) (32666)
8     4                                             (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
  • 重量级锁分为自旋锁及重量级锁
    • 如果轻量级锁失败,可能直接升级为重量级锁,也可能尝试尝试自旋锁
    • 乐观地认为线程线程可以很快获得锁,可以让线程自旋(空循环),并不直接采用操作系统的互斥操作
    • 如果自旋成功,可以避免操作系统的互斥操作
    • 如果自旋失败,依然会升级为重量级锁
  • 当执行完锁代码时,锁标识位仍然为10重量级锁状态
  • 重量级锁的加锁过程
    • 当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列的队首,然后调用park函数挂起当前线程。在linux系统上,park函数底层调用的是gclib库的pthread_cond_wait,JDK的ReentrantLock底层也是用该方法挂起线程的
    • 如果线程获得锁后调用Object#wait方法,则会将线程加入到WaitSet中,当被Object#notify唤醒后,会将线程从WaitSet移动到cxq或EntryList中去。需要注意的是,当调用一个锁对象的wait或notify方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。
  • 自旋锁默认启用,自旋次数不宜设置过大(避免长时间占用CPU),-XX:+UseSpinning -XX:PreBlockSpin=10
    • 1.7之后的版本已经去掉了参数UseSpinning及PreBlockSpin,自旋锁内置实现,自旋次数自适应动态调整
  • 重量级锁时,执行完锁代码块标识位仍然为10,ObjectMonitor地址也不变

总结

  • 当开启偏向锁时,默认为偏向锁
  • 当某实例存在锁竞争时,升级为轻量级锁,如果发生了批量重偏向,则不会升级为轻量级锁
  • 当偏向锁撤销超过某数量时,直接取消该class的偏向锁
  • 当执行完成锁代码块后,标识为无锁
  • 当某实例存在并发竞争时,升级为重量级锁
  • 重量级锁分为自旋和重量级锁两种
  • 当执行完成锁代码块后,重量级锁并不会再被标识为无锁

  • 测试代码中禁止使用Thread.join(),会影响测试结果
  • 由于Thread.start()方法是synchronized标识的方法,所以如果需要用到多线程,要确认start()方法锁的对象,与欲测试的锁对象互不干扰

锁类

public class LockObject {
    public synchronized void lockMethod() {
        System.out.println("in synchronized method!!!");
        print();
        System.out.println("out synchronized method!!!");

    }

    public void print() {
        System.out.println(ClassLayout.parseInstance(this).toPrintable());
        System.out.println(Thread.currentThread().getId());
        System.out.println("====================================");
    }
}

无锁

public class NoLockApp {

    public static void main(String[] args) throws Exception {
        System.out.println("服务启动n秒后才会启动偏向锁,故直接打印对象为无锁状态");
        System.out.println("====================================");
        LockObject app = new LockObject();
        app.print();
    }
}

偏向锁代码


public class BiasedLockApp {

    public static void main(String[] args) throws Exception {
        Thread.sleep(10000);//当注释该行时,因偏向锁延迟5s启动,故synchronized中print时为轻量级锁
        LockObject app = new LockObject();
        app.print();
        app.lockMethod();
        app.print();

        Thread.sleep(100*1000);
    }
}

轻量级锁

Thread.sleep(10 * 1000);
LockObject lock = new LockObject();
lock.lockMethod();//偏向锁
new Thread(() -> {
    lock.lockMethod();//出现竞争,升级为轻量级锁
}).start();
Thread.sleep(3 * 1000);
lock.print();//轻量级锁,执行完成锁代码后,标识为无锁
lock.lockMethod();//该实例后续加锁不会再使用偏向锁
lock.print();

重量级锁

LockObject lock = new LockObject();
new Thread(() -> {
    lock.lockMethod();
}, "thread-1").start();
new Thread(() -> {
    lock.lockMethod();
}, "thread-2").start();
Thread.sleep(1000);
lock.print();//重量级锁时,执行完锁代码块标识位仍然为10,ObjectMonitor地址也不变

lockrecord

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                    // the lock, must be double word aligned
  oop       _obj;                                     // object holds the lock;

class BasicLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  volatile markOop _displaced_header;

ObjectMonitor

ObjectMonitor() {
    _header       = NULL;//监视器所属对象的mark word;
    _count        = 0; // 重入次数
    _waiters      = 0,
    _recursions   = 0;//用来表示某个线程进入该锁的次数。
    _object       = NULL;//监视器所属的对象;
    _owner        = NULL; // 获得锁的线程
    _WaitSet      = NULL; // 调用wait()方法被阻塞的线程 
    _WaitSetLock  = 0 ;//保护等待队列  作用于自旋锁
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;//cxq是一个单向链表。被挂起线程等待重新竞争锁的链表, monitor 通过CAS将包装成ObjectWaiter写入到列表的头部。为了避免插入和取出元素的竞争,所以Owner会从列表尾部取元素
    FreeNext      = NULL ;
    _EntryList    = NULL ; // EntryList是一个双向链表。当EntryList为空,cxq不为空,Owener会在unlock时,将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程为OnDeck线程。 
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;//_owner变量是线程指针时为1,是锁记录指针时是0;
    _previous_owner_tid = 0;
}

cxq与entryList的区别:在cxq中的队列可以继续自旋等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起。而EntryList中的线程都是被挂起的线程。

参考