Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Latest commit

 

History

History
History
422 lines (255 loc) · 12.3 KB

File metadata and controls

422 lines (255 loc) · 12.3 KB
Copy raw file
Download raw file
Outline
Edit and raw actions

synchronized / Lock

  1. JDK 1.5之前

    ,Java通过

    synchronized

    关键字来实现

    功能

    • synchronized是JVM实现的内置锁,锁的获取和释放都是由JVM隐式实现的
  2. JDK 1.5

    ,并发包中新增了

    Lock接口

    来实现锁功能

    • 提供了与synchronized类似的同步功能,但需要显式获取和释放锁
  3. Lock同步锁是基于

    Java

    实现的,而synchronized是基于底层操作系统的

    Mutex Lock

    实现的

    • 每次获取和释放锁都会带来用户态和内核态的切换,从而增加系统的性能开销
    • 在锁竞争激烈的情况下,synchronized同步锁的性能很糟糕
    • JDK 1.5,在单线程重复申请锁的情况下,synchronized锁性能要比Lock的性能差很多
  4. JDK 1.6,Java对synchronized同步锁做了充分的优化,甚至在某些场景下,它的性能已经超越了Lock同步锁

实现原理

复制

public class SyncTest {
    public synchronized void method1() {
    }

    public void method2() {
        Object o = new Object();
        synchronized (o) {
        }
    }
}

复制

$ javac -encoding UTF-8 SyncTest.java
$ javap -v SyncTest

修饰方法

复制

public synchronized void method1();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=0, locals=1, args_size=1
       0: return
  1. JVM使用ACC_SYNCHRONIZED访问标识来区分一个方法是否为同步方法

  2. 在方法调用时,会检查方法是否被设置了

    ACC_SYNCHRONIZED

    访问标识

    • 如果是,执行线程会将先尝试持有Monitor对象,再执行方法,方法执行完成后,最后释放Monitor对象

修饰代码块

复制

public void method2();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=4, args_size=1
       0: new           #2                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: astore_1
       8: aload_1
       9: dup
      10: astore_2
      11: monitorenter
      12: aload_2
      13: monitorexit
      14: goto          22
      17: astore_3
      18: aload_2
      19: monitorexit
      20: aload_3
      21: athrow
      22: return
  1. synchronized修饰同步代码块时,由monitorentermonitorexit指令来实现同步
  2. 进入monitorenter指令后,线程将持有Monitor对象,进入monitorexit指令,线程将释放Monitor对象

管程模型

  1. JVM中的同步是基于进入和退出管程Monitor)对象实现的

  2. 每个Java对象实例都会有一个Monitor,Monitor可以和Java对象实例一起被创建和销毁

  3. Monitor是由ObjectMonitor实现的,对应ObjectMonitor.hpp

  4. 当多个线程同时访问一段同步代码时,会先被放在EntryList

  5. 当线程获取到Java对象的Monitor时(Monitor是依靠

    底层操作系统

    Mutex Lock

    来实现

    互斥

    的)

    • 线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex
  6. 进入

    WaitSet

    • 竞争锁失败的线程会进入WaitSet
    • 竞争锁成功的线程如果调用wait方法,就会释放当前持有的Mutex,并且该线程会进入WaitSet
    • 进入WaitSet的进程会等待下一次唤醒,然后进入EntryList重新排队
  7. 如果当前线程顺利执行完方法,也会释放Mutex

  8. Monitor依赖于底层操作系统的实现,存在用户态内核态之间切换,所以增加了性能开销

img

复制

ObjectMonitor() {
  _header       = NULL;
  _count        = 0;        // 记录个数
  _waiters      = 0,
  _recursions   = 0;
  _object       = NULL;
  _owner        = NULL;     // 持有该Monitor的线程
  _WaitSet      = NULL;     // 处于wait状态的线程,会被加入 _WaitSet
  _WaitSetLock  = 0 ;
  _Responsible  = NULL ;
  _succ         = NULL ;
  _cxq          = NULL ;
  FreeNext      = NULL ;
  _EntryList    = NULL ;    // 多个线程访问同步块或同步方法,会首先被加入 _EntryList
  _SpinFreq     = 0 ;
  _SpinClock    = 0 ;
  OwnerIsThread = 0 ;
  _previous_owner_tid = 0;
}

锁升级优化

  1. 为了提升性能,在JDK 1.6引入偏向锁、轻量级锁、重量级锁,用来减少锁竞争带来的上下文切换
  2. 借助JDK 1.6新增的Java对象头,实现了锁升级功能

Java对象头

  1. JDK 1.6的JVM中,对象实例在堆内存中被分为三部分:对象头实例数据对齐填充
  2. 对象头的组成部分:Mark Word指向类的指针数组长度(可选,数组类型时才有)
  3. Mark Word记录了对象有关的信息,在64位的JVM中,Mark Word为64 bit
  4. 锁升级功能主要依赖于Mark Word中锁标志位是否偏向锁标志位
  5. synchronized同步锁的升级优化路径:偏向锁 -> 轻量级锁 -> 重量级锁

img

偏向锁

  1. 偏向锁主要用来优化同一线程多次申请同一个锁的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源

  2. 偏向锁的作用

    • 当一个线程再次访问同一个同步代码时,该线程只需对该对象头的Mark Word中去判断是否有偏向锁指向它
    • 无需再进入Monitor去竞争对象(避免用户态和内核态的切换
  3. 当对象被当做同步锁,并有一个线程抢到锁时

    • 锁标志位还是01,是否偏向锁标志位设置为1,并且记录抢到锁的线程ID,进入偏向锁状态
  4. 偏向锁

    不会主动释放锁

    • 当线程1再次获取锁时,会比较当前线程的ID锁对象头部的线程ID是否一致,如果一致,无需CAS来抢占锁

    • 如果不一致,需要查看

      锁对象头部记录的线程

      是否存活

      • 如果没有存活,那么锁对象被重置为无锁状态(也是一种撤销),然后重新偏向线程2

      • 如果

        存活

        ,查找线程1的栈帧信息

        • 如果线程1还是需要继续持有该锁对象,那么暂停线程1(STW),撤销偏向锁升级为轻量级锁
        • 如果线程1不再使用该锁对象,那么将该锁对象设为无锁状态(也是一种撤销),然后重新偏向线程2
  5. 一旦出现其他线程竞争锁资源时,偏向锁就会被

    撤销

    • 偏向锁的撤销可能需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法
    • 如果还没有执行完,说明此刻有多个线程竞争,升级为轻量级锁;如果已经执行完毕,唤醒其他线程继续CAS抢占
  6. 高并发

    场景下,当

    大量线程

    同时竞争同一个锁资源时,偏向锁会被

    撤销

    ,发生

    STW

    ,加大了

    性能开销

    • 默认配置

      • -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000
      • 默认开启偏向锁,并且延迟生效,因为JVM刚启动时竞争非常激烈
    • 关闭偏向锁

      • -XX:-UseBiasedLocking
    • 直接

      设置为重量级锁

      • -XX:+UseHeavyMonitors

红线流程部分:偏向锁的获取撤销 img

轻量级锁

  1. 当有另外一个线程竞争锁时,由于该锁处于偏向锁状态

  2. 发现对象头Mark Word中的线程ID不是自己的线程ID,该线程就会执行

    CAS

    操作获取锁

    • 如果获取成功,直接替换Mark Word中的线程ID为自己的线程ID,该锁会保持偏向锁状态
    • 如果获取失败,说明当前锁有一定的竞争,将偏向锁升级为轻量级锁
  3. 线程获取轻量级锁时会有两步

    • 先把锁对象的Mark Word复制一份到线程的栈帧中(DisplacedMarkWord),主要为了保留现场!!
    • 然后使用CAS,把对象头中的内容替换为线程栈帧中DisplacedMarkWord的地址
  4. 场景

    • 在线程1复制对象头Mark Word的同时(CAS之前),线程2也准备获取锁,也复制了对象头Mark Word
    • 在线程2进行CAS时,发现线程1已经把对象头换了,线程2的CAS失败,线程2会尝试使用自旋锁来等待线程1释放锁
  5. 轻量级锁的适用场景:线程交替执行同步块,绝大部分的锁在整个同步周期内都不存在长时间的竞争

红线流程部分:升级轻量级锁 img

自旋锁 / 重量级锁

  1. 轻量级锁

    CAS

    抢占失败,线程将会被挂起进入

    阻塞

    状态

    • 如果正在持有锁的线程在很短的时间内释放锁资源,那么进入阻塞状态的线程被唤醒后又要重新抢占锁资源
  2. JVM提供了自旋锁,可以通过自旋的方式不断尝试获取锁,从而避免线程被挂起阻塞

  3. JDK 1.7

    开始,

    自旋锁默认启用

    ,自旋次数

    不建议设置过大

    (意味着

    长时间占用CPU

    • -XX:+UseSpinning -XX:PreBlockSpin=10
  4. 自旋锁重试之后如果依然抢锁失败,同步锁会升级至

    重量级锁

    ,锁标志位为

    10

    • 在这个状态下,未抢到锁的线程都会进入Monitor,之后会被阻塞在WaitSet
  5. 锁竞争不激烈

    锁占用时间非常短

    的场景下,自旋锁可以提高系统性能

    • 一旦锁竞争激烈或者锁占用的时间过长,自旋锁将会导致大量的线程一直处于CAS重试状态占用CPU资源
  6. 高并发

    的场景下,可以通过

    关闭自旋锁

    来优化系统性能

    • -XX:-UseSpinning
      
      • 关闭自旋锁优化
    • -XX:PreBlockSpin
      
      • 默认的自旋次数,在JDK 1.7后,由JVM控制

img

小结

  1. JVM在JDK 1.6中引入了分级锁机制来优化synchronized

  2. 当一个线程获取锁时,首先对象锁成为一个

    偏向锁

    • 这是为了避免在同一线程重复获取同一把锁时,用户态和内核态频繁切换
  3. 如果有多个线程竞争锁资源,锁将会升级为

    轻量级锁

    • 这适用于在短时间内持有锁,且分锁交替切换的场景
    • 轻量级锁还结合了自旋锁避免线程用户态与内核态的频繁切换
  4. 如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁

  5. 优化synchronized同步锁的关键:

    减少锁竞争

    • 应该尽量使synchronized同步锁处于轻量级锁偏向锁,这样才能提高synchronized同步锁的性能
    • 常用手段
      • 减少锁粒度:降低锁竞争
      • 减少锁的持有时间,提高synchronized同步锁在自旋时获取锁资源的成功率,避免升级为重量级锁
  6. 锁竞争激烈时,可以考虑禁用偏向锁禁用自旋锁

Morty Proxy This is a proxified and sanitized view of the page, visit original site.