synchronized可以保证原子性,有序性和可见性
另一个关键字volatile可以保证有序性和可见性不能保证原子性
synchronized能保证同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
synchronized底层是如何实现的呢?
将带有synchronized的代码段进行反编译,会发现synchronized关键字编译为字节码变为了两条指令,分别是monitorenter和monitorexit。
其实真正的锁对象是monitor对象。monitor是由JVM创建的一个C++对象。
montor对象这里可以理解为一种同步工具,或同步机制,被描述为一个对象。由JVM生成。在hotSpot虚拟机中,monitor是由ObjectMonitor实现的。
ObjectMonitor主要定义如下:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 线程的重入次数
_object = NULL; // 存储该monitor的对象
_owner = NULL; // 标识拥有该monitor的线程
_WaitSet = NULL; // 处于wait状态的线程,会被加入到
_WaitSetLock = 0 ;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL; // 多线程竞争锁时的单向列表
FreeNext = NULL;
_EntryList = NULL; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread = 0;
}
monitor并不是随着对象的创建而创建,我们是通过synchronized修饰符告诉JVM需要我们为某个对象创建相关联的monitor对象。
每个线程都存在两个ObjectMonitor对象列表,分别为free和used列表,同时JVM中为维护着global locklist。当线程需要ObjectMonitor对象时,首先从线程自身的free表中申请,如果没有空闲的,就从global list中申请。
monitor对象内部有两个重要的成员变量:
owner:拥有这把锁的线程
recursions:记录线程拥有锁的次数
每个对象都与一个监视器monitor相关联,当且仅当拥有所有者时,即被拥有时,monitor就会被锁定。其他线程无法来获取该monitor。
当JVM执行到monitorenter时,会尝试获取获得当前对象对应的monitor的所有权,过程如下:
monitor会出现在方法结束和异常处,这样做的目的是保障出现异常的时候也能保证锁被释放。JVM保障每个monitor都有对应的monitorexit。
Object中除了拥有者和重入次数之外,还有几个重要的变量。
其中:
_waitSet 中存储了wait状态下的线程
_cxq:竞争队列
_EntryLis 中存储了处于阻塞状态的线程
状态转换图如下所示:
_owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线 程安全的。
_cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指 向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
_EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中。
_WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。
这里需要补充一点,synchronized修饰方法的时候,字节码中并不是使用monitorenter和monitorexit来实现的,而是在方法中添加了ACC_SYNCHRONIZED修饰,会隐式的调用monitorenter和 monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit
对象如何跟monitor关联的呢?与对象相关的monitor信息存储在哪里?
这就要从对象在内存的布局讲起,
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对象填充(Padding)。
对象头:主要分为两部分,MarkWord,和klass pointor:
Mark Word存储自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、 线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
klass pointer:这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
实例数据:对象真正存储的信息,存放类的属性数据信息,包括父类的属性信息
对齐填充:由于虚拟机的要求,对象的起始地址必须是8的字节的整数倍。如果不满足这个要求,可能会需要对齐填充部分。
为什么说synchronized是重量级锁呢?究其原因,还是因为monitor是重量级锁。在ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数, 执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就 会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语 言中是一个重量级(Heavyweight)的操作。
主要是内核态和用户态之间的切换带来了大量的系统资源的消耗,这就是在synchronized未优化之前,效率低下的原因。
synchronized是java中的一个重量级锁,他依赖的是monitor对象,通过monitorenter和monitorexit两条指令完成对共享代码块的保护。对象的monitor信息存储在对象头的Mark Word中,Mark Word的长度和虚拟机上位长相等。synchronized是重量级锁是因为在加锁和解锁的过程中调用了很多内核态的功能,需要不断的进行内核态和用户态的切换,造成性能较差。
不可打断性
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直等待,不可被中断
可重入性
一个线程可以多次执行synchronized,重复获取同一把锁。
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.
可重入的好处:
* 可以避免死锁
* 可以更好的封装代码
参考链接:
[1] https://www.cnblogs.com/xuxinstyle/p/13387851.html
[2] 黑马程序员面试视频笔记