Synchronized

并发编程三大特性

Synchronized获取锁的过程

  1. 获取锁
  2. 清空本地内存
  3. 将主内存中的变量拷贝到本地内存中
  4. 执行代码块
  5. 将本地内存中的变量刷新到主存中
  6. 释放锁
  7. Synchronized会在代码块前后添加内存屏障,保证代码块的内容不会重排序

原子性

  1. 在添加了Synchronized后,会在代码的前后增加**monitorenter****monitorexit**
  2. 从而在执行代码块时需要先获取到锁
  3. 因为在同一个时间段内只能有一个线程获取到锁,因此同一时间段内只会有一个线程执行代码块,从而能够保证原子性

有序性

  1. Synchronized不能够保证线程内的不进行指令重排序
  2. 但因为Synchronized确保了代码块只能由一个线程执行,而指令重排序的**as if serial**原则确保了,在单线程环境下不改变结果,因此Synchronized能够保证有序性

可见性

  1. 在释放锁前会将本地内存中的变量刷入到主内存中,保证其他线程能够读取到最新的数据
  2. 在获取锁前,会先清空本地内存中的数据,保证读取到的都是主内存中最新的数据
  3. Synchronized只能够保证,获取同一个Monitor锁的线程的可见性,对于不是同一个Monitor锁的线程不能够保证可见性

image.png

可重入

什么是可重入锁

  1. 当线程通过Synchronized获取到锁后,可以继续调用其他的相同对象锁的Synchronized代码块
  2. 可以避免死锁

如何实现

  1. 内部封装了线程id和计数器,如果此时计数器为0,则说明当前没有线程占有锁
  2. 如果此时有一个线程想来获取锁,会使用CAS机制将对象的线程id更换为自己的线程id,并且将计数器增加1
  3. 如果此时该线程想要继续执行相同对象锁的Synchronized代码块,则获取锁后计数器+1

可重入锁和不可重入锁的区别

  1. 不可重入锁不能再继续调用其他的同步代码块代码
  2. 不可重入锁在退出代码块后,则直接释放锁,可重入锁在退出代码块后,会将锁的计数器-1

不可中断

  1. Synchronized代码块中如果正在与其他线程竞争同一把锁,在该线程获取到该锁之前,将会**一直阻塞等待**,不能中断阻塞的过程。
  2. Lock方法中如果是调用**lock**方法,则也是属于**不可中断**,在没有获取到锁前会一直阻塞等待,如果调用的是**tryLock**方法,则线程会在等待一定的时间后结束等待,因此tryLock方法属于是**可中断**

Synchronized和Lock的区别

  1. Synchronized是一个**关键字**,Lock是一个**接口**
  2. Synchronized**不用手动释放锁**,即使代码块内抛出异常,仍然能够执行 **monitorexit**方法从而将锁释放,而lock**需要手动释放锁**,否则其他线程无法获取到锁
  3. Synchronized是**不可中断锁**,在获取锁时会一直阻塞,lock可以**tryLock**方法选择是否可中断
  4. Synchronized无法判断线程是否获取到锁,lock可以判断到线程此时是否获取到锁
  5. lock可以使用读锁来提高多线程读的效率
  6. Synchronized是非公平锁即不保证先阻塞的先获取锁,lock可以选择是公平锁和非公平锁,即保证先阻塞的先获取到锁

Synchronized原理

加锁原理

  1. 对于封装了Synchronized的代码块,会在代码块的前后添加**monitorenter****monitorexit**指令,同时在结尾仍然会添加一个**monitorexit**指令,从而保证即使Synchronized代码块中抛出异常,也能够正常释放锁。
  2. 对于封装了Synchronized的方法,会在方法体前添加质量,会等同于在方法的执行前后添加monitorenter和monitorexit指令

monitor本质

  1. **monitor**是由JVM底层维护,真正的对象是C++中的**ObjectMonitor**
  2. 对象保存在堆中,对象除了对象本身,还包含对象头信息,对象头信息与ObjectMonitor相关联
  3. **recursions**是线程的重入次数,每当进入一个代码块时,recursions则会+1,当退出一个代码块时,recursions则会-1
  4. **owner**是持有当前Monitor的线程,如果说明owner相同则说明是同一个线程可以再次获取到Monitor
  5. **waitSet**,当一个线程**曾经持有过锁**,但因为某些条件不足而释放锁时则会进入该双向循环链表
  6. **cxq**,当线程第一次进入Synchronized代码块,第一次争抢锁时,则会进入cxq单向链表
  7. **EntryList**,当cxq中的线程第一次获取锁失败后,cxq中的线程则会加入到EntryList中
//结构体如下
ObjectMonitor::ObjectMonitor() {  
    _header       = NULL;  
    _count       = 0;  
    _waiters      = 0,  
    _recursions   = 0;       //线程的重入次数
    _object       = NULL;  	 //绑定的是对象本身
    _owner        = NULL;    //标识拥有该monitor的线程
    _WaitSet      = NULL;    //等待线程组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;  
    _Responsible  = NULL ;  
    _succ         = NULL ;  
    _cxq          = NULL ;    //多线程竞争锁进入时的单向链表
    FreeNext      = NULL ;  
    _EntryList    = NULL ;    //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
    _SpinFreq     = 0 ;  
    _SpinClock    = 0 ;  
    OwnerIsThread = 0 ;  
}

锁竞争

最终会调用ObjectMonitor::enter方法

  1. 线程会使用CAS的方式将owner属性设置为当前线程的值
  2. 如果owner属性与当前的线程相等,则说明当前持有Monitor的线程再次请求Monitor,则会将recursions+1,记录当前锁重入的次数
  3. 如果recursions为0,则会将Monitor的owner属性设置为当前线程的属性,并且将recursions设置为1
  4. 如果获取锁失败,则会等待锁的释放

锁等待

如果锁竞争失败后则会调用ObjectMonitor::enteri方法

  1. 会将该现成封装成ObjectWaiter对象,并且状态设置为CXQ
  2. 通过CAS的方式将Node对象添加到CXQ队列中,因为此时可能会有多个线程同时进行CXQ列表的添加操作,因此需要进行一直循环操作
  3. 当线程被添加到CXQ队列中,仍然会继续自旋尝试获取锁,如果还是没有获取到锁,则会调用park将该线程挂起
  4. 当被其他线程唤醒后,则会继续调用trylock方法尝试获取锁

Synchronized为什么是重量级锁

因为在锁的竞争和等待使用了大量park和unpark等操作,这些操作都需要系统转换到内核态进行操作,而用户态到内核态的转换是十分消耗资源的,因此Synchronized是重量级锁

Synchronized锁升级过程

Java对象在堆中的存储

Java对象头

Mark Word

对象头的字节是反向存储的
image.png

Klass

实例数据

对象填充

偏向锁(biased_lock=1 && 锁标志位 01)

为什么需要偏向锁优化
  1. 轻量级锁不涉及到线程的挂起,已经开销较小,只需要进行CAS操作进行锁的获取
  2. 但是在大多数的情况下,一个锁资源只会被一个线程所获取,而不会出现锁竞争的情况,但此时该线程仍然需要频繁进行CAS操作
  3. 因此可以才有偏向锁进行优化,在没有锁竞争的情况下,只需要进行比较Mark Word上的Thread Id与获取锁的Thread Id进行比较是否相等,不需要进行CAS操作
为什么会有匿名偏向锁
  1. 偏向锁可以选择是否打开,当开启偏向锁后,锁对象在创建时会进入**4s的匿名偏向锁状态**、此时锁对象内保存的线程id是0,线程在获取锁时会将线程id变为自己的
  2. 偏向锁优化的场景在于 在**没有线程争抢的情况**下,可以起到优化的效果。在有线程争抢的情况下,需要频繁的进行锁撤销和锁升级流程,因为需要进行锁撤销流程,锁撤销需要等待**全局安全点**,因此效率较低。
  3. 但是在JVM虚拟机启动时,会有很多默认线程会启动,其中会有很多Synchronized代码块,此时会存在线程争抢的问题,如果直接使用偏向锁,会出现频繁的锁升级和锁撤销。
  4. 在JDK6后的版本中,**偏向锁默认为开启状态**,只要对象创建出来后,就是开启了偏向锁状态,**在JDK15后移除偏向锁**
  5. 此时如果Mark word已经被标记为偏向锁,则只需要将当前线程的id与锁内保存的线程id进行对比,如果相同则直接获取到锁
偏向锁撤销(因为条件不足)
  1. 调用对象的**HashCode**方法,以及**偏向锁出现争抢**,会导致偏向锁被撤销,
  2. 偏向锁的撤销需要等待**全局安全点**,此时所有线程暂停
  3. 遍历线程的栈帧,判断是否存在锁记录,如果存在锁记录则需要将其变为无锁状态
  4. 将当前线程唤醒,升级为轻量级锁,进行CAS竞争

轻量级锁(锁标志位 00)

偏向锁升级为轻量级锁
  1. 首先判断锁标志位为**偏向锁**(biased_lock=1 && 锁标志位 01)
  2. 此时判断**Mark Word**内的**Thread Id**是否与当前线程相同,如果相同则直接获取到偏向锁
  3. 如果不相同,则需要进行CAS操作将对象Mark Word内的Thread Id替换为线程自己的Thread Id
  4. 如果CAS操作成功,则成功获取到偏向锁,如果失败则说明出现**线程争抢**,此时该线程会**阻塞**等待**全局安全点**
  5. 当全局安全点时,如果持有锁的线程已经**退出同步代码块**,则会**撤销偏向锁**
  6. 如果持有锁的线程仍然在同步代码块中,则会在持有锁的线程中创建一个**锁记录**(Lock Record),并且将这个锁记录与锁对象中的Mark Word做CAS操作,此时偏向锁升级为轻量级锁。
  7. 唤醒线程继续执行

image.png
image.png

解锁流程
  1. 当对象**不为NULL**值时,通过CAS操作将线程内存储的**Mark Word**与对象的锁记录进行交换,如果成功则解锁
  2. 如果CAS操作失败,则说明此时轻量级锁已经膨胀为重量级锁,需要走重量级锁的解锁流程。
  3. 此时会通过锁中的Monitor地址,找到对应的Monitor对象,然后通过CAS操作将Owner设置为null,然后唤醒EntryList中的等待线程进行争抢

重量级锁

轻量级锁升级为重量级锁
  1. 首先判断锁状态是否为轻量锁状态(锁标志位 00)
  2. 在当前线程内创建**锁记录**(Lock Record),与锁对象内**Mark Word**进行CAS操作
  3. 如果CAS操作成功,则成功获取到轻量级锁,如果CAS操作失败,会有两种情况,**持有锁的是线程是自己**,此时会在当前线程中再创建一条锁记录。另一种情况是,**其他线程已经持有了轻量级锁**,此时该线程会继续进行**自旋操作**争抢锁
  4. 在自旋争抢锁的过程中会进入到**轻量级锁升级为重量级锁**的流程,在升级的过程中**仍然会尝试获取**轻量级锁,如果持有锁的线程已经将锁释放,则该线程能够成功获取到轻量级锁,不用升级为重量级锁。
  5. 在升级为重量级锁的过程中,该线程会为锁对象创建一个**Monitor**对象,并让原持有锁的线程**Mark Word**指向Monitor对象。
  6. 并且Monitor对象的owner属性指向持有锁的线程,同时争抢锁的线程进入Monitor中的 EntryList中进行阻塞等待

image.png
image.png

释放锁流程

当持有重量级锁的线程释放锁时,会通过CAS的方式将Monitor对象中的Owner设置为null,并且唤醒EntryList中的线程进行争抢

锁消除

  1. 即时编译器(JIT)会根据逃逸分析的数据支持,因为在代码中会有很多情况下会加入Synchronized
  2. 如果根据分析判断出堆上的内容不会被其他线程锁访问,即可以不需要进行同步操作,从而节约资源

锁粗化

  1. JVM会分析到如果有连续的加锁操作,会判断是否可以将其范围扩大,从而减少加锁的操作,减少性能损耗
  2. 例如在循环内有Synchronized同步代码块,JVM会将同步代码块的范围扩大到循环外,减少锁竞争
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值