Java synchronized 关键字详解

概述

我们知道 volatile 关键字可以保证共享变量的可见性和有序性,但不能保证原子性,要想同时满足三者,可以使用 synchronized 关键字

synchronized 是对象锁,也就是说它锁的是一个对象,适用的对象具体表现为以下三种形式:

  • 对于普通同步方法,锁的是当前实例对象

    public synchronized void add() {
      i++;
    }
    
  • 对于静态同步方法,锁的是当前类的 Class 对象

    public static synchronized void add() {
      i++;
    }
    
  • 对于代码块,锁是 synchronized 括号里配置的对象

    public void add() {
    	// 锁的是当前实例对象
      synchronized (this) {
        i++;
      }
      // 锁的是当前类的 Class 对象
      synchronized (*.class) {
        i++;
      }
    }
    

如果锁的是实例对象,那么该锁只能锁住当前实例对象,不能锁住其他实例对象,哪怕它们同属一个类。假设有同属一个类的实例对象 A 和实例对象 B,同时调用它们的加锁方法,并不能保证线程安全

但如果加锁方法是 static 修饰的,即锁的是类对象,那么就能保证线程安全了。因为 A 和 B 都同属一个类,类对象只有一个,该类的所有实例共用一个类对象


synchronized 原理

jvm 基于进入和退出 monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。monitor 对象被称为管程或监视器锁,在 Java中,每一个对象都会关联一个 monitor 对象。当 monitor 对象被线程持有后,就把 monitor 对象的_ower 标志位设为该线程 id,对应的对象便处于锁定状态,等到线程释放锁,_ower 又会被重置。另外,竞争锁失败的线程会被加入到对应 monitor 对象的阻塞队列,等待获取锁

代码块同步使用 monitorenter 和 monitorexit 指令实现,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。线程执行到 monitorenter 指令时,会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。线程执行到 monitorexit 指令时,线程释放 monitor,其他线程才有机会持有 monitor

方法同步并没有使用 monitorenter 和 moniterexit 指令,而是在方法的 flag 加入了 ACC_SYNCHRONIZED 的标记位。当线程执行该方法会判断是否有 ACC_SYNCHRONIZED 标志,如果有则尝试获取 monitor 对象,方法结束则释放,执行步骤与代码块同步一致


synchronized 锁优化

之前提到的加锁过程称为重量级锁,会影响程序性能。为了减少获得锁和释放锁带来的性能消耗,从 JavaSE 1.6 开始引入了偏向锁和轻量级锁。锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,这是为了提高获得锁和释放锁的效率

1. 偏向锁

研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问,不存在多线程争用的情况,就会使用偏向锁,线程不需要触发同步就能获得锁,降低获得锁的代价

前面说过,synchronized 锁的是一个对象,使用偏向锁时,会在对象的对象头的 Mark Word 记录锁偏向的线程 id,并把 Mark Work 中偏向锁的标识是否设置成 1(表示当前是偏向锁)。以后该线程再次请求锁,无需再做任何同步操作,即可获取锁,从而提升性能

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行)。它首先会暂停拥有偏向锁的线程,判断持有偏向锁的线程是否活动,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,撤销偏向锁后恢复到未锁定或轻量级锁的状态

偏向锁在 Java6 和 Java7 里是默认开启的,但是它在应用程序启动几秒之后才激活,如有必要可以使用 JVM 参数来关闭延迟:-XX:BiasedLockingStartupDelay = 0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过 JVM 参数来关闭偏向锁:-XX:-UseBiasedLocking = false,那么程序默认会进入轻量级锁状态

下图是偏向锁的获得和撤销流程

2. 轻量级锁

monitorenter 与 monitorexit 是依赖操作系统的互斥量来实现的。互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作,为了优化性能,从 Java6 开始引入了轻量级锁的概念。轻量级锁本意是为了减少多线程进入互斥的几率,并不是要替代互斥,它利用了 CPU 原语 Compare-And-Swap(CAS),尝试在进入互斥前,进行补救

线程在获取锁之前,先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针,如果成功,当前线程获得锁,否则表示其他线程竞争锁,当前线程尝试使用自旋来获取锁

轻量级锁解锁时,线程会使用原子的 CAS 操作将 Dispatch Mark Word 替换回到对象头,如果成功,表示没有竞争发生;如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

下图是轻量级锁及膨胀流程图

3. 锁的优缺点对比
优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁赊销的消耗适用于只有一个线程访问同步块场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗 CPU追求响应时间,同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗 CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较长

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值