synchronized解读
对象结构
对象结构介绍
HotSpot虚拟机
中,对象在内存中存储的布局可以分为三个区域:对象头(Header)
、实例数据(Instance Data)
、对齐填充(Padding)
mark-word
: 对象标记字段,占4个字节,用于存储一些列的标记位,例如: 哈希值、轻量级锁的标记位、偏向锁标记位、分代年龄等Klass Pointer
: Class对象的类型指针,JDK1.8默认开启指针压缩后占4个字节,关闭指针压缩后(-XX:-UseCompressedOops
)长度为8个字节,其指向的位置是对象对应的Class对象(对应的元数据对象)的内存地址- 对象实际数据: 包括对象的所有成员变量,大小由哥哥成员变量决定,比如:
byte = 1个字节(8比特位)
、int = 4个字节(32比特位)
- 对齐: 非必须,起占位符作用,由于
HotSpot虚拟机
的内存管理系统要求对象起始地址必须是8字节的整数倍,所以对象头正好是8字节的倍数,当对象实例数据部分没有对齐时,就要通过对齐填充来补全
- 另外在
mark-word
锁类型标记中,无锁、偏向锁、轻量锁、重量锁以及GC标记,5种锁类型没法用2比特标记(2比特最终有4种组合:00
、01
、10
、11
),所以无锁、偏向锁前又占了一位偏向锁标记,最终001
为无锁,101
为偏向锁
Monitor
对象
- 在
HotSpot虚拟机
中monitor
是由C++中的ObjectMonitor
实现的 synchronized
的运行机制就是当JVM
监测到对象在不同竞争状况时,会自动切换到合适的锁实现,即锁的升级、降级- 三种不同的
Monitor
的实现就是常说的三种不同的锁: 偏向锁(Biased Locking
)、轻量级锁和重量级锁,当一个Monitor
被某个线程持有之后,它便处于锁定状态 Monitor
主要数据结构:源码地址: jdk8/hotspot/file/vm/runtime/objectMonitor.hpp// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0; // 线程重入次数
_object = NULL; // 存储 Monitor 对象
_owner = NULL; // 持有当前线程的 owner
_WaitSet = NULL; // 处于wait状态的线程,会被加入到 _WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 单向列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}ObjectMonitor
: 有两个队列:_WaitSet
和_EntryList
,用于保存ObjectWaiter
对象列表_owner
: 获取Monitor
对象的线程进入_owner
时,_count-1
,如果线程调用了wait()
方法,此时会释放Monitor
对象,_owner
恢复为空,_count-1
,同时该等待线程进入_WaitSet
中等待被唤醒- 锁执行效果:
每个Java对象头中都包括Monitor
对象(存储的指针的指向),synchronized
也是通过这种方式获取锁,因此synchronized()
传入任何对象都能获取锁
synchronized 特性
原子性
- 原子性指一个操作不是不可中断的,只能全部执行成功或者全部执行失败
- 测试: 反编译后的指令码:
private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
for (int i1 = 0; i1 < 10000; i1++) {
add();
}
});
thread.start();
}
// 等10个线程运行完毕
Thread.sleep(1000);
System.out.println(counter);
}
public static void add() {
synchronized (AtomicityTest.class) {
counter++;
}
}同步方法:public static void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: ldc #12 // class org/itstack/interview/AtomicityTest
2: dup
3: astore_0
4: monitorenter
5: getstatic #10 // Field counter:I
8: iconst_1
9: iadd
10: putstatic #10 // Field counter:I
13: aload_0
14: monitorexit
15: goto 23
18: astore_1
19: aload_0
20: monitorexit
21: aload_1
22: athrow
23: return
Exception table:ACC_SYNCHRONIZED
是一个同步标识,对应的16进制值是0x0020
,线程进入方法时,都会判断是否有此标识,然后开始竞争Monitor
对象
同步代码:monitorenter
: 在判断拥有同步标识ACC_SYNCHRONIZED
后抢先进入此方法的线程会优先拥有Monitor
的owner
,此时计数器 +1monitorexit
: 当执行完成推出后,计数器 -1,归 0 后被其他进入的线程获取
可见性
- 为什么添加
synchronized
后能保证变量的可见性- 线程解锁前,必须把共享内存的最新值刷新到主内存中
- 线程加锁前,将清空工作内存中共享变量的值,从而导致使用共享变量时需要从主内存中重新读取最新的值
volatile
的可见性通过内存屏障(Memory Barrier
)来实现,而synchronized
靠操作系统内核互斥锁实现,相当于 JMM 中的lock
、unlock
,退出代码块时刷新变量到主内存
有序性
as-if-serial: 保证不管编译器和处理器为了性能优化会如何进行指令重排序,都需要保证单线程下运行结果的正确性,即: 在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是无序的
双重检验锁(
Double-checked Locking
):public class Singleton {
private Singleton() {
}
private volatile static Singleton instance;
public Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}synchronized
的有序性并不是volatile
的防止指令重排序,因此即使synchronized
拥有可见性的特点,还是需要使用volatile
关键字- 不加
volatile
可能导致的结果: 第一个线程在初始化对象设置instance
指向内存地址时,第二个线程进入,有指令重排序,在判断if(instance == null)
时有可能出错,因为此时instance
可能还没初始化完成