参考文献

引言

  • Java编程语言提供了多种用于线程通信的机制.这些方法中最基础的就是同步(synchronization),它是使用监视器(monitor)实现的.Java中每个对象都与一个线程可以锁定或解锁的监视器相关联.在任何时刻,只有一个线程可以持有某个监视器上的锁.任何其他试图锁定该监视器线程都将阻塞,直到它们可以获得该监视器上的锁.

  • 线程T可以多次锁定某个特定的监视器,而每个解锁操作都会抵消一次锁定操作的效果.(可重入锁)

synchronized

  • synchronized语句是对对象的引用,即基于对象实现,然后它试图执行在该对象的监视器上的锁定动作,并且在锁定动作成功完成之前,不会执行下一步动作.在锁定动作执行之后,synchronized语句体被执行.如果该语句体执行结束,无论是正常结束还是异常结束,都会在相同的监视器上执行解锁动作.

  • synchronized的底层实际上就是依靠的Monitor.Monitor 被翻译为监视器或管程,每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word(标记信息,包含锁,GC等标记信息) 中就被设置指向 Monitor 对象的指针.

    img
    • 对象内置锁ObjectMonitor流程:
      • 监控区部分(Entry Set),停留在这个区域的线程由于还没有获得对象操作权限的原因,依然停留在synchronized同步块以外,具体来说就是synchronized(Object)这句代码的位置.处于“Entry Set”区域的线程,其线程状态被标识为BLOCKED

      • 对象操作权持有区(The Owner),对于一个特定对象的Object Monitor控制来说,一个时间点最多有一个线程处于这个区域.它的状态变为RUNNABLE.

      • 所有曾经获得过锁,但是由于其它必要条件不满足而需要wait的时候,线程就进入了对象锁的wait set区域 .它的状态变为WAITING

        • wait set区域的线程获得Notify/notifyAll通知的时候,随机的一个Thread(Notify)或者是全部的Thread(NotifyALL)从对象锁的wait set区域进入了Entry Set中.
      • 在当前拥有锁的线程释放掉锁的时候,处于该对象锁的Entry Set区域的线程都会抢占该锁,但是只能有任意的一个Thread能取得该锁,而其他线程依然在Entry Set中等待下次来抢占到锁之后再执行.

  • ObjectMonitor.hpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56

    //ObjectMonitor.hpp

    ObjectMonitor() {
    // 记录无锁状态的Mark Word
    _header = NULL;
    _count = 0;
    // 等待锁的线程个数
    _waiters = 0,
    // 线程重入次数
    _recursions = 0;
    // 指向的对象头
    _object = NULL;
    // 指向线程或者Lock Record,即指向了当前持有锁线程
    _owner = NULL;
    // 调用wait()方法后等待锁的队列,即条件变量的等待队列
    _WaitSet = NULL;
    // 等待队列的锁
    _WaitSetLock = 0 ;
    _Responsible = NULL ;
    // 下一个被唤醒的线程
    _succ = NULL ;
    // ObjectWaiter 队列
    _cxq = NULL ;
    FreeNext = NULL ;
    // ObjectWaiter 队列,即同步队列
    _EntryList = NULL ;

    _SpinFreq = 0 ;
    _SpinClock = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;

    }


    class ObjectWaiter : public StackObj {
    public:
    enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ };

    // 指向下一个节点
    ObjectWaiter * volatile _next;

    // 指向上一个节点
    ObjectWaiter * volatile _prev;

    // 线程
    Thread* _thread;

    // 线程状态
    volatile TStates TState;

    public:
    ObjectWaiter(Thread* thread);

    };
    • header : 记录无锁状态的Mark Word,同Lock RecordDisplaced Mark Word字段
    • ObjectWaiter : 一个双向链表结构对象,封装了线程的信息,包括当前线程的状态
    • EntryList : 当多个线程同时访问同步代码时,这些因自旋到达次数后还没有竞争到锁的线程就会进入阻塞,将封装成ObjectWaiter结点进入EntrySet双向链表(EntryList);而获取到锁的线程,即获取到锁对象的Monitor,Monitor依赖于操作系统底层的Mutex Lock,当线程获取到Mutex Lock时,其他线程将不能获取到Mutex Lock.
    • WaitSet : 当线程调用wait()方法,该线程将主动释放Mutex Lock,并且进入阻塞,将封装成ObjectWaiter结点进入WaitSet双向链表,等待下一次被其他线程调用notify()或者notifyAll()唤醒开始继续竞争锁;
    • owner : 加锁和释放锁是通过owner,即指向了持有锁的线程的Lock Record或者当前持有锁线程
    • recursions : 线程重入次数,即表示Synchronized支持可重入
    • object : 指向了对象头
    • cxq : cxq队列存储的是指向enter函数的时候因为锁已经被其他线程占有而阻塞的线程(单向链表)
    • succ : 下一个被唤醒线程
  • synchronized方法在被调用时会自动执行锁定动作,它的方法体在该锁定动作成功完成之前,是不会被执行的.如果该方法是实例方法,那么它会锁定与在其上调用该方法的实例(即,在该方法体执行期间被称为this的对象)相关联的监视器.

    1
    2
    3
    4
    5
    6
    7
    8
    public synchronized void testThread(){

    }
    // <==>
    public void testThread(){
    synchronized(this){
    }
    }
  • 如果该方法是static的,那么它会锁定与表示定义该方法的类的Class对象相关联的监视器.如果该方法体执行结束,无论是正常结束还是异常结束,都会在相同的监视器上执行解锁动作.

    1
    2
    3
    4
    5
    6
    7
    8
     public synchronized static void testThread(){

    }
    // <==>
    public void testThread(){
    synchronized(Test.class){
    }
    }
  • JVM基于进入和退出Monitor对象来实现方法同步和代码块同步.代码块同步是使用monitorentermonitorexit指令实现的.

  • monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对.任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态.线程执行到monitorenter指定时,将会尝试获取对象对应的monitor的所有权,即尝试获得对象的锁.

锁消除

  • 锁消除是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除.

  • 举个例子让大家更好理解.

    1
    2
    3
    4
    5
    6
    public String test(String s1, String s2){
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(s1);
    stringBuffer.append(s2);
    return stringBuffer.toString();
    }
    • 上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来.

    • test 方法中三个变量s1, s2, stringBuffer, 它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的.

    • 我们都知道 StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除.

      1
      2
      3
      4
      5
      6
      7
      8
      StringBuffer.class

      // append 是同步方法
      public synchronized StringBuffer append(String str) {
      toStringCache = null;
      super.append(str);
      return this;
      }

Java对象头

  • synchronized用的锁是存在Java对象头里的.

    • 如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头;

      1
      2
      3
      4
      5
      |---------------------------------------------------------------------------------|
      | Object Header (96 bits) |
      |--------------------------------|-----------------------|------------------------|
      | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
      |--------------------------------|-----------------------|------------------------|
    • 如果对象是非数组类型,则用2个字宽存储对象头.在32位虚拟机中,1字宽等于4字节,即32bit.

      1
      2
      3
      4
      5
      |--------------------------------------------------------------|
      | Object Header (64 bits) |
      |------------------------------------|-------------------------|
      | Mark Word (32 bits) | Klass Word (32 bits) |
      |------------------------------------|-------------------------|
  • Java对象头有三部分组成:Mark Word,指向类的指针,数组长度(只有数组对象才有);

    长度 内容 说明
    32/64bit Mark Word 存储对象的hashCode或锁信息等
    32/64bit Class Metadata Address 存储到对象类型数据的指针
    32/64bit Array Length 数组的长度(如果当前对象是数组)
    • Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位.
    对象的HashCode(25bit) 对象分代年龄(4bit) 是否是偏向锁(1bit) 锁标志位(2bit) 锁状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29

    |-------------------------------------------------------|--------------------|
    | Mark Word (32 bits) | State |
    |-------------------------------------------------------|--------------------|
    | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
    |-------------------------------------------------------|--------------------|
    | thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
    |-------------------------------------------------------|--------------------|
    | ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
    |-------------------------------------------------------|--------------------|
    | ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
    |-------------------------------------------------------|--------------------|
    | | lock:2 | Marked for GC |
    |-------------------------------------------------------|--------------------|


    |------------------------------------------------------------------------------|--------------------|
    | Mark Word (64 bits) | State |
    |------------------------------------------------------------------------------|--------------------|
    | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |
    |------------------------------------------------------------------------------|--------------------|
    | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased |
    |------------------------------------------------------------------------------|--------------------|
    | ptr_to_lock_record:62 | lock:2 | Lightweight Locked |
    |------------------------------------------------------------------------------|--------------------|
    | ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked |
    |------------------------------------------------------------------------------|--------------------|
    | CMS过程用到的标记信息 | lock:2 | Marked for GC |
    |------------------------------------------------------------------------------|--------------------|

锁的状态

  • 在JavaSE1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态,偏向锁状态,轻量级锁状态重量级锁状态.这几种状态会随着竞争情况逐渐升级,锁可以升级但不能降级.意味着偏向锁升级成轻量级锁后不能降级成偏向锁.
    • 这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率.
biased_lock lock 状态
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记

偏向锁(Biased Locking)

  • HotSpot的作者经研究发现,大多数情况下,锁不仅不存在多线程竞争(没有竞争,就自己这个线程),而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁.即只有一个线程进入临界区,偏向锁.
  • 当一个线程访问同步块并获得锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否在存储着指向当前线程的偏向锁.如果测试成功,表示当前线程已经获得了锁.如果测试失败,则需要在测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程.
    • Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁.
    • 偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入.
  • 一个对象创建时,如果开启了偏向锁(在JDK6和JDK7默认开启),对象的Mark Word的值为Ox05即最后三位为101,此时他的thread,epoch,age都是0;
    • 但是偏向锁默认是有延迟的,不会再程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态;如果想避免延迟,可加VM参数-XX:BasicLockingStartupDelay=0来禁用延迟;
    • 如果没有开启偏向锁,那么对象创建后,Mark Word值为Ox01即最后三位为001,这时它的hashcode,age都为0,第一次用到hashcode时赋值;
    • 禁用偏向锁,添加VM参数:-XX:UserBaisedLocking=false
img
偏向锁的撤销
  • 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁.
  • 偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码).
    • 它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;
    • 如果线程仍然活着,拥有偏向锁的栈会被执行遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复要无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程.
  • 触发撤销偏向锁的方法
    • 调用对象hashCode方法
      • 调用了对象的hashCode但偏向锁的对象Mark Word中存储的是线程id,如果调用hashCode会导致偏向锁被撤销 ;
        • 轻量级锁会在锁记录中记录hashCode
        • 重量级锁会在锁记录中记录hashCode
    • 其他线程使用对象
      • 当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁;
    • 调用wait-notify

轻量级锁(Lightweight Locked)

  • 线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁.即多个线程交替进入临界区,轻量级锁.

  • 场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(可以认为没有竞争),那么就可以使用轻量级说来优化.

轻量级锁加锁
  • 线程在执行同步块之前,JVM会现在当前线程的栈帧中创建用于存储记录的空间,并将对象头的Mark Word复制到锁记录中,官方称为Displaced Mark Word,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针.
    • 如果成功,当前线程获得锁,如果失败,表示其他线程在竞争锁,当前线程便尝试使用自旋来获得锁.
轻量级锁解锁
  • 轻量级锁解锁,会使用原子的CAS操作将Displaced Mark Word替换回对象头,如果成功,则表示没有竞争发生.如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁.
  • 因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态.
    • 当锁处于这个状态,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的竞争锁.
轻量级锁加锁以及膨胀流程
img
  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象,内部可以存储锁定对象的mark word(不再一开始就使用Monitor);

    img
  • 让锁记录中的Object reference指向锁对象(Object),并尝试用cas去替换Object中的mark word,将此mark word放入lock record中保存;

    img
  • 如果CAS替换成功,则将Object的对象头替换为锁记录的地址状态 00(轻量级锁状态),并由该线程给对象加锁;

    img
  • 如果一个线程在给一个对象加轻量级锁时,CAS替换操作失败(因为此时其他线程已经给对象加了轻量级锁),此时该线程就会进入锁膨胀过程;

    img
  • 此时便会给对象加上重量级锁(使用Monitor)

    • 将对象头的Mark Word改为Monitor的地址,并且状态改为01(重量级锁)
    • 并且该线程放入入EntryList中,并进入阻塞状态BLOCKED
    img
  • 当Thread-0退出同步块解锁时,使用CASMark Word的值恢复给对象头,失败,这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象设置Owner为null,唤醒EntryListBLOCKED线程;

重量级锁( Heavyweight Locked)

  • 如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞.
  • 升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态.
  • 在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁
  • 多个线程同时进入临界区,重量级锁

锁的优缺点对比

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