synchronized详解


JAVA后端开发知识总结(持续更新…)


synchronized详解



一、synchronized简介

  synchronized关键字可以解决多个线程之间的同步性,对三大性质都能兼顾到,是一种重量级锁。由于JAVA的线程是映射到操作系统的原生内核线程上的,因此阻塞或唤醒线程,都需要操作系统介入,需要从用户态转换到内核态,这使得早期的synchronized性能极其低下。随着Java 1.6对synchronized的优化,特别是锁升级,减少了获得锁和释放锁带来的性能消耗。

  • synchronized的使用
  1. 普通同步方法:锁为当前实例对象
  2. 静态同步方法:锁为当前类的Class对象
  3. 同步代码块:锁为括号里自定义对象

在这里插入图片描述

  • synchronized的特点
  1. 可重入性
  2. 非公平锁
  3. 互斥性
  4. 保证原子性、可见性、有序性

在这里插入图片描述

二、synchronized的实现原理

  在JVM规范里可以看到,不管是方法同步还是代码块同步都是基于进入和退出monitor对象来实现的,通过javap对class字节码文件反编译可以观察到反编译后的代码。

2.1 synchronized的字节码解析

  使用javap -c Synchronize可以查看编译之后的具体信息,了解JVM底层是怎么实现synchronized的。当synchronized分别作用在同步代码块和方法上时,其底层有所区别。

  • 修饰同步代码块
public static void main(String[] args) {
    synchronized (TestSyn.class){
        System.out.println("test");
    }
}

在这里插入图片描述

  当使用了synchronized后,会在同步代码块的入口和出口分别添加了monitorentermonitorexit指令。当执行monitorenter指令时,线程获取对象的锁也就是monitor对象的持有权,若计数器为0则可以获取,获取后计数器加1。在执行monitorexit指令后,计数器减1,当为0时表明锁被释放,释放monitor。JVM必须保证monitorentry和monitorexit成对出现

  • 修饰方法

  如果synchronized修饰方法,就不会有上面两个指令,而是一个ACC_SYNCHRONIZED标识,该标识指明了该方法加锁了,是一个同步方法,JVM通过该标识执行相应的同步调用。

  此时线程在执行方法前会先去常量池获取对象的monitor,如果获取成功则执行方法代码,如果monitor已被其它线程获取,则当前线程被阻塞。

在这里插入图片描述

2.2 monitor对象

  任何对象都有一个monitor与之对应,当这个对象的monitor被持有以后,它将处于锁定状态。对于每个对象的对象头,其在重量级锁状态下时对应的标志位是10,同时会存储指向重量级监视器锁的指针,而这个监视器就是monitor。synchronized便是通过这种方式获取对象锁的,这也是JAVA中任意对象可以作为锁的原因。

  在Hotspot虚拟机中,对象的monitor由ObjectMonitor对象实现,其跟同步相关的对象属性如下所示:

ObjectMonitor() {    
	 _header       = NULL;  //MarkOop对象头
	 
	 _count        = 0;     //用来记录该对象被线程获取锁的次数    
	 
	 _waiters      = 0;     //等待的线程数
	 
	 _recursions   = 0;     //锁的重入次数    
	 
	 _owner        = NULL;  //指向持有ObjectMonitor对象的线程     
	 
	 _WaitSet      = NULL;  //调用wait()方法,处于wait状态的线程,会被加入到_WaitSet  
	   
	 _WaitSetLock  = 0 ;    
	 
	 _EntryList    = NULL ; //处于等待锁的阻塞状态的线程,会被加入到该列表  
}

monitor的基本运行机制如下图所示
在这里插入图片描述

  1. 对于一个synchronized修饰的方法或代码块,当多个线程同时访问时,这些线程会先被放进 _EntryList队列,使之处于阻塞状态。
  2. 对于一个想要获得monitor的线程,如果没有其它线程持有monitor,它就会和_EntryList队列中的线程及_WaitSet队列中被唤醒的线程进行竞争。
  3. 当一个线程获取到了对象的monitor后,就可以进入运行状态。此时,ObjectMonitor的 _owner指向当前线程,_count加1表示当前对象锁被一个线程获取。
  4. 当运行状态的线程调用 wait() 方法后,会释放monitor对象,进入等待状态。此时,ObjectMonitor的_owner变为null,_count减1,同时线程进入 _WaitSet队列挂起,直到有线程调用notify()方法唤醒该线程。
  5. 如果当前线程执行完毕,释放monitor对象,ObjectMonitor的_owner变为null,_count减为0。

三、锁优化

  synchronized是一种重量级锁,会涉及到操作系统状态的切换,效率十分低下,所以JDK1.6中对synchronized进行了各种优化,实现了高效并发。对象头monitor是实现优化的基础和关键。

3.1 JAVA对象头

  这一部分可以参考 《深入理解Java虚拟机》 中的对象内存布局部分。简而言之,对象的内存中包含三个部分,其中之一就是对象头。对象头又大致分为两部分,其中一部分存储了指向元空间中的类信息的指针,另一部分即是Mark Word,存储了对象的锁状态、HashCode、分代年龄等运行时数据。

  Java对象头的Mark Word是一个非固定的动态数据结构,对于32位的HopSpot虚拟机的Mark Word,在对象未被锁定时,其存储内容如下所示:

在这里插入图片描述

  对象除了未被锁定的状态外,还有轻量级锁定、重量级锁定、GC标记、可偏向等多种状态:

在这里插入图片描述

3.2 偏向锁

  引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的同步操作,连CAS都去掉了。对比轻量级锁的获取及释放需要多次CAS,偏向锁只需要在记录线程ID时进行一次CAS操作。

  • 当锁对象第一次被线程获取时
  1. 访问Mark Word中将偏向锁的标识设置成1,锁标志位设为01。
  2. CAS操作记录线程ID到Mark Word中,然后执行同步代码。
  3. 如果另一个线程进行竞争,偏向模式结束,开始解锁,并根据锁对象的锁定状态决定是否撤销偏向。
  4. 如果对象未锁定,撤销偏向,锁的标识设置成0;如果已锁定,将会升级成轻量级锁,改锁标志位为00。

注意

  由于对象的HashCode也是存储在对象头中的,这和偏向锁存储线程ID的空间产生了冲突,因此,当一个对象经过了一次一致性哈希计算后,就再也无法进入偏向锁状态了。

偏向锁的流程图片来源于《Synchronized关键字深析》):

在这里插入图片描述

3.3 轻量级锁

  偏向锁获取不成功,线程不会被立即挂起,还会使用轻量级锁。轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。它只是简单地将对象头作为指针指向持有锁的线程堆栈的内部。其基本步骤如下所示:

  1. 在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为01),虚拟机首先将在当前线程的栈帧中建立一个锁记录(Lock Record)空间,用于存储锁对象目前的Mark Word的拷贝。然后拷贝对象头中的Mark Word到锁记录中。
  2. 然后,虚拟机进行CAS操作将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指回对象。
  3. 如果更新成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为00
  4. 如果更新操作失败,虚拟机首先检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,可以直接进入同步块。
  5. 不是则说明存在多线程竞争,此时轻量级锁就要膨胀为重量级锁,膨胀之前会进行几次自适应自旋,以做最后的尝试。
  6. 如果在自旋过程中释放了锁,那么竞争的线程获取轻量级锁成功。
  7. 如果在自旋结束也未能获取轻量锁,锁升级为重量级锁,此时原线程持有ObjectMonitor对象,而竞争线程会加入EntryList,进入阻塞状态。
  8. 膨胀为重量级锁时,锁标志的状态值变为10,此时Mark Word存储指向重量级锁(monitor)的指针,后面等待锁的线程也要进入阻塞状态。
  • 轻量级锁的释放
  1. 通过CAS操作把线程中的Displaced Mark Word和对象的Mark Word进行替换。
  2. 如果替换成功,整个同步过程完成。
  3. 如果替换失败,说明有其它线程尝试过获取该锁,就需要在释放锁的同时,唤醒被挂起的线程。

轻量级锁的流程图片来源于《Synchronized关键字深析》):

在这里插入图片描述

3.4 其它锁优化

  • 自适应自旋

  为了让线程等待,在轻量级锁升级前,让线程执行几个忙循环(自旋锁),JDK1.6 加入了自适应自旋,即如果某个锁自旋很少成功获得,那么下一次就会减少自旋。

  • 锁消除

  锁消除指的就是虚拟机即使编译器在运行时,如果检测到不可能存在竞争的共享数据,就要执行锁消除。它是基于逃逸分析的。

  • 锁粗化

  例如某段代码中,同一个对象反复加锁,那么可以将加锁范围扩展到整个代码逻辑上面。

四、synchronized 和 ReenTrantLock

  1. 两者都是可重入锁,同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
  2. synchronized的实现依赖于JVM,而ReenTrantLock依赖于JDK(即JAVA逻辑代码)。
  3. ReenTrantLock比synchronized增加了一些高级功能:等待可中断;公平锁;绑定多个条件(Condition对象)

五、synchronized 和 volatile

  1. volatile只能实现操作的可见性和有序性,原子操作主要是基于CAS,不能完全保证。
  2. volatile只能修饰变量,而synchronized都既可以修饰变量,又可以修饰方法。

六、锁的降级

  • 多个线程同时竞争升级为重量级锁。
  • 都执行完毕之后,锁对象变成了无锁。
  • 此时再有一个线程去争抢锁,就从无锁变成了轻量级锁。
  • 当重量级锁释放了之后,锁对象是无锁的,有新的线程来竞争的话又会从轻量级锁开始。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值