Synchronized同步锁

一、概述


        同步锁是一种保证线程安全的机制。就是使用Synchronized(语法级)关键字将一段代码逻辑用同一把锁锁起来,哪个线程竞争到了这把锁哪个线程才有权访问这段代码,其余线程只能阻塞等待,保证了同一时刻只有一个线程执行上锁代码,保证了代码的原子性,从而确保线程安全。

 

二、 Synchronized关键字


         每个Java对象都可以充当一个实现同步的锁,这些锁被称为内置锁( Intrinsic Lock ) 或者监视器锁( Monitor Lock )。

  • 修饰实例方法:默认使用当前对象this作为锁。
  • 修饰静态方法:默认使用当前对象的Class对象作为锁。
  • 修饰代码块:指定的某个Java对象作为锁。
  • 当一个线程访问对象的一个 synchronized(this) 同步代码块时,另一个线程仍然可以访 问该对象中的非 synchronized(this) 同步代码块。
  • 父类中 synchronized 修饰的方法,如果子类没有重写,则该方法仍然是线程安全性;如果子类重写,并且没有使用 synchronized 修饰,则该方法不是线程安全的;
  • 在定义接口方法时,不能使用 synchronized 关键字;
  • 构造方法不能使用 synchronized 关键字,但可以使用 synchronized 代码块来进行同 步;
  • 离开 synchronized 代码块后,该线程所持有的锁,会自动释放;

 

三、实现机制


        Java5之前synchronized是仅有的线程同步手段。在加锁和解锁的过程中依赖操作系统互斥锁(重量级锁)实现,操作系统需要从用户态转换为内核态,切换成本非常高,所以早期synchronized效率低。

        Java6之后JVM对synchronized锁进行了优化,新增了锁升级的概念,不会一上来就是互斥锁,提升了synchronized的效率。

        synchronized代码块其实是由一对monitorenter/monitorexit指令实现的,synchronized是通过对象内部的监视器( monitor )来实现的,线程通过执行monitorenter指令尝试获取monitor的所有权,当monitor被占用时就会处于锁定状态。

 

四、监视器


        每个对象都有一个监视器,线程通过执行 monitorenter 指令尝试获取monitor的所有权,当monitor被占用时就会处于锁定状态。

 

获取 monitor 的所有权的过程如下:

  1. 如果 monitor 的进入数为 0 ,则该线程进入 monitor ,然后将进入数设置为1 ,该线程即为 monitor 的所有者,代表持有锁;
  2. 如果线程已经占有该monitor ,只是重新进入,则进入 monitor 的进入数加 +1 ;
  3. 如果其他线程已经占用了monitor ,则该线程进入阻塞状态,直到monitor 的进入数为为0,再重新尝试获取monitor的所有权; 

 

五、锁升级


        Java6之后JVM提供了三种不同的Monitor实现,也就是三种不同的锁:偏斜锁、轻量级锁、重量级锁。        

        所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的 竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

5.1 偏向锁

        偏向锁是为了在单线程(没有出现多个线程并发)执行情况下,尽量减少不必要的轻量级锁 执行路径,该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。因为轻量级锁的加锁与释放锁,也需要多次执行CAS原子指令。而偏向锁只需要在切换线程设置 ThreadID的时候,执行一次CAS原子指令。所以,偏向锁的作用是在只有一个线程执行同步块时,进一步提高性能。

        当没有线程并发出现时,默认会使用偏斜锁。 JVM会利用CAS操作,在对象头上的Mark Word部分设置线程ID ,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。 如果有另外的线程试图锁定某个已经被偏斜过的对象, JVM 就需要撤销偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

5.2 轻量级锁

        根据轻量级锁的实现,虽然轻量级锁不支持并发,遇到并发就要升级为重量级锁。 但是轻量级锁可以支持多个线程以串行的方式访问同一个加锁对象。但是,每次执行,都消耗了重复的加锁与解锁的性能开销。

        锁的状态(Mark Word)保存在对象头中。在 Hotspot 虚拟机中,一个 JAVA 对象的存储结构,在内存 中的存储布局分为 3 块区域:对象头( Header )、实例数据( Instance Data )和对齐 填充( Padding )。

image.png

  •  lock 标志位:2位二进制,锁状态标记位。
  • age Java对象年龄:在 GC 中,如果对象在 Survivor 区复制一次,年龄增加 1 。当对 象达到设定的阈值时,将会晋升到老年代。
  • thread :持有偏向锁的线程ID。
  • ptr_to_lock_record :指向栈中锁记录的指针。

5.2.1 轻量级锁的加锁过程

458df2d22f63ae5b5af51e499168a4d9.png

  1. 在代码进入同步块的时候,如果对象锁状态为无锁状态(lock标志位“ 01 ”, biased_lock标志位“ 0 ”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录( Lock Record )的空间,用于存储锁对象目前的 Mark Word 的拷贝,官方命名为 Displaced Mark Word 。
  2. 拷贝对象头中的 Mark Word 复制到锁记录( Lock Record )中。
  3. 拷贝成功后,虚拟机将尝试将对象的 Mark Word 中的ptr_to_lock_record更新为指向 Loc k Record 的指针,并将 Lock record 里的 owner 指针指向到对象的 Mark Word 。如 果更新成功,则执行步骤4,否则执行步骤5。
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的 lock标志位设置为“ 00 ”,即表示此对象处于轻量级锁定状态。
  5. 如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否已经指向当前线程的栈帧。如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争该对象的锁,轻量级锁就要升级为重量级锁 ,lock标志位的状 态值变为“ 10 ”, Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待 锁的线程也要进入阻塞状态。 当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

 5.2.2 轻量级锁的解锁过程

  1. 通过 CAS 指令,尝试把线程中复制的 Displaced Mark Word 对象替换当前的 Mark Word 。
  2. 如果替换成功,整个同步过程就完成了。
  3. 如果替换失败,说明有其他线程尝试过获取该锁,该锁已升级为重量级锁 ,那就要在释放锁的同时,通知其它线程重新参与锁的竞争。

5.3 重量级锁 

        当 JVM 检测到多个线程并发执行时会将锁升级成重量级锁。重量级锁是依赖于操作系统互斥锁( Mutex Lock )所实现的锁。操作系统的互斥锁实现线程之间的切换,需要从用户态转换到核心态,切换成本非常高,状态之间的转换需要相对比较长的时间, 这是早期 Synchronized 效率低的原因。因此,这种依赖于操作系统互斥锁( Mutex Lock ) 所实现的锁,称之为“重量级锁”。

 

六、总结


 

image.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值