Synchronized全面介绍

Synchronized

并发编程的三个重要特性:

  1. 原子性:一个操作或者多次操作,要么所有操作全部得到执行,并且不会受到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。
  2. 可见性:当一个线程对共享变量进行修改,另外的线程可以立即得到修改后的最新的值。
  3. 有序性:程序在多线程下,程序中代码执行的顺序是代码的先后执行的。程序代码不会因为指令重排序(程序为了优化执行效率从而打乱了代码的执行顺序)而打乱,从而导致程序产生不同的结果。

synchronized可以保证原子性,有序性和可见性
另一个关键字volatile可以保证有序性和可见性不能保证原子性

Synchronized执行原理

synchronized能保证同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。

synchronized底层是如何实现的呢?

将带有synchronized的代码段进行反编译,会发现synchronized关键字编译为字节码变为了两条指令,分别是monitorenter和monitorexit。

Synchronized全面介绍_第1张图片
其实真正的锁对象是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:记录线程拥有锁的次数

monitorenter

每个对象都与一个监视器monitor相关联,当且仅当拥有所有者时,即被拥有时,monitor就会被锁定。其他线程无法来获取该monitor。
当JVM执行到monitorenter时,会尝试获取获得当前对象对应的monitor的所有权,过程如下:

  1. 若此时monitor的进入数为0,线程可以进入monitor,并将moitor的进入数置为1。此时当前线程作为monitor的所有者。
  2. 若当前线程已经拥有monitor的所有权,允许重入monitor,则进入monitor的进入数+1.(可重入也是synchronized锁的一个特性。可重入特性即一个线程可以重复获得同一把锁)
  3. 如果其他线程已经占有了monitor的所有权,当前尝试获得monitor的所有权的线程会阻塞。知道monitor的进入数为0,才能重新尝试获得monitor的所有权。

monitorexit:

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程
  2. 执行monitorexit会将monitor的进入数-1,当monitor的进入数为0时,线程退出了monitor,此时其他被这个monitor阻塞的线程可以尝试获得这个monitor的所有权。

monitor会出现在方法结束和异常处,这样做的目的是保障出现异常的时候也能保证锁被释放。JVM保障每个monitor都有对应的monitorexit。

状态转变:

Object中除了拥有者和重入次数之外,还有几个重要的变量。
其中:
_waitSet 中存储了wait状态下的线程
_cxq:竞争队列
_EntryLis 中存储了处于阻塞状态的线程

状态转换图如下所示:

Synchronized全面介绍_第2张图片
_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 修饰方法

这里需要补充一点,synchronized修饰方法的时候,字节码中并不是使用monitorenter和monitorexit来实现的,而是在方法中添加了ACC_SYNCHRONIZED修饰,会隐式的调用monitorenter和 monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit

对象与monitor关联

对象如何跟monitor关联的呢?与对象相关的monitor信息存储在哪里?
这就要从对象在内存的布局讲起,

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对象填充(Padding)。

Synchronized全面介绍_第3张图片

对象头:主要分为两部分,MarkWord,和klass pointor:
Mark Word存储自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、 线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
Synchronized全面介绍_第4张图片
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的特性:

  1. 不可打断性
    一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直等待,不可被中断

  2. 可重入性
    一个线程可以多次执行synchronized,重复获取同一把锁。
    synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.
    可重入的好处:
    * 可以避免死锁
    * 可以更好的封装代码

参考链接:

[1] https://www.cnblogs.com/xuxinstyle/p/13387851.html
[2] 黑马程序员面试视频笔记

你可能感兴趣的:(java)