synchronized详解
问题
- 某实例未执行过锁代码块,该实例的标识位是101还是001?
- 轻量级锁和重量级锁执行完成锁代码块后的标识位分别是什么?
- 轻量级锁执行完锁代码块后,出现其他线程竞争锁,该线程竞争成功后是什么锁?
- synchronized是否可降级?
java对象Mark Word
32位Header Mark Word
即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中的线程都是被挂起的线程。