Java-中关于锁的一些理解

8次阅读

共计 3697 个字符,预计需要花费 10 分钟才能阅读完成。

jdk 6 对锁进行了优化, 让他看起来不再那么笨重,synchronized 有三种形式: 偏向锁, 轻量级锁, 重量级锁.

介绍三种锁之前, 引入几个接下来会出现的概念

mark work:
对象头, 对象头中存储了一些对象的信息, 这个是锁的根本, 任何锁都需要依赖 mark word 来维持锁的运作, 对象头中存储了当前持有锁的线程,hashCode,GC 的一些信息都存储在对象头中.
在 JVM 中,对象在内存中除了本身的数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark word 和类型指针。另外对于数组而言还会有一份记录数组长度的数据.
类型指针是指向该对象所属类对象的指针,mark word 用于存储对象的 HashCode、GC 分代年龄、锁状态等信息。在 32 位系统上 mark word 长度为 32bit,64 位系统上长度为 64bit。为了能在有限的空间里存储下更多的数据,其存储格式是不固定的,在 32 位系统上各状态的格式如下:

可以看到锁信息也是存在于对象的 mark word 中的。当对象状态为偏向锁时,mark word 存储的是偏向的线程 ID;当状态为轻量级锁时,mark word 存储的是指向线程栈中 Lock Record 的指针;当状态为重量级锁时,为指向堆中的 monitor 对象的指针.

Lock Record:
前面对象头中提到了 Lock Record, 接下来说下 Lock Record,Lock Record 存在于线程栈中, 翻译过来就是锁记录, 它会拷贝一份对象头中的 mark word 信息到自己的线程栈中去, 这个拷贝的 mark word 称为 Displaced Mark Word , 另外还有一个指针指向对象

monitor:
monitor 存在于堆中, 什么是 Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。

与一切皆对象一样,所有的 Java 对象是天生的 Monitor,每一个 Java 对象都有成为 Monitor 的潜质,因为在 Java 的设计中,每一个 Java 对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者 Monitor 锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联(对象头的 MarkWord 中的 LockWord 指向 monitor 的起始地址),同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下:

  • Owner:初始时为 NULL 表示当前没有任何线程拥有该 monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为 NULL
  • EntryQ: 关联一个系统互斥锁(semaphore),阻塞所有试图锁住 monitor record 失败的线程
  • RcThis: 表示 blocked 或 waiting 在该 monitor record 上的所有线程的个数
  • Nest: 用来实现重入锁的计数
  • HashCode: 保存从对象头拷贝过来的 HashCode 值(可能还包含 GC age)
  • Candidate: 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate 只有两种可能的值 0 表示没有需要唤醒的线程 1 表示要唤醒一个继任线程来竞争锁
    (摘自:Java 中 synchronized 的实现原理与应用)

说完几个关键概念之后来说一下锁的问题:

  1. 偏向锁
    偏向锁是锁的级别中最低的锁, 举个例子: 在此 demo 中, 获得操作 list 的一直都是 main 线程, 没有第二个线程参与操作, 此时的锁就是偏向锁, 偏向锁很轻,jdk 1.6 默认开启, 当第一个线程进入的时候, 对象头中的 threadid 为 0, 表示未偏向任何线程, 也叫做匿名偏向量

    public class SyncDemo1 {public static void main(String[] args) {SyncDemo1 syncDemo1 = new SyncDemo1();
           for (int i = 0; i < 100; i++) {syncDemo1.addString("test:" + i);
           }
       }
    
       private List<String> list = new ArrayList<>();
    
       public synchronized void addString(String s) {list.add(s);
       }
    
    }
    
    

当第一个线程进入的时候发现是匿名偏向状态, 则会用 cas 指令把 mark words 中的 threadid 替换为当前线程的 id 如果替换成功, 则证明成功拿到锁, 失败则锁膨胀;
当线程第二次进入同步块时, 如果发现线程 id 和对象头中的偏向线程 id 一致, 则经过一些比较之后, 在当前线程栈的 lock record 中添加一个空的 Displaced Mark Word, 由于操作的是私有线程栈, 所以不需要 cas 操作,synchronized 带来的开销基本可以忽略;
当其他线程进入同步块中时, 发现偏向线程不是当前线程, 则进入到撤销偏向锁的逻辑, 当达到全局安全点时, 锁开始膨胀为轻量级锁, 原来的线程仍然持有锁, 如果发现偏向线程挂了, 那么就把对象的头改为无锁状态, 锁膨胀

  1. 轻量锁

当锁膨胀为轻量级锁时, 首先判断是否有线程持有锁 (判断 mark work), 如果是, 则在当前线程栈中创建一个 lock record 复制 mark word 并且 cas 的把当前线程栈的 lock record 的地址放到对象头中, 如果成功, 则说明获取到轻量级锁, 如果失败, 则说明锁已经被占用了, 此时记录线程的重入次数 (把 lock record 的 mark word 设置为 null), 锁会自旋可以进行自适应性自旋, 确保在竞争不激烈的情况下仍然可以不膨胀为重量级锁从而减少消耗, 如果 cas 失败, 则说明线程出现竞争, 需要膨胀为重量级的锁, 代码如下:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
  // 如果是无锁状态
  if (mark->is_neutral()) {
    // 设置 Displaced Mark Word 并替换对象头的 mark word
    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {TEVENT (slow_enter: release stacklock) ;
      return ;
    }
  } else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    // 如果是重入,则设置 Displaced Mark Word 为 null
    lock->set_displaced_header(NULL);
    return;
  }

  ...
  // 走到这一步说明已经是存在多个线程竞争锁了 需要膨胀为重量级锁
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
  1. 重量锁

重量级锁就是我们传统意义上的锁了, 当线程发生竞争, 锁膨胀为重量级锁, 对象的 mark word 指向堆中的 monitor, 此时会将线程封装为一个 objectwaiter 对象插入到 monitor 中的 contextList 中去, 然后暂停当前线程, 当持有锁的线程释放线程之前, 会把 contextList 里面的所有线程对象插入到 EntryList 中去, 会从 EntryList 中挑选一个线程唤醒,被选中的线程叫做 Heir presumptive 即假定继承人(应该是这样翻译),就是图中的 Ready Thread,假定继承人被唤醒后会尝试获得锁,但 synchronized 是非公平的,所以假定继承人不一定能获得锁(这也是它叫 ” 假定 ” 继承人的原因)。

如果线程获得锁后调用 Object#wait 方法,则会将线程加入到 WaitSet 中,当被 Object#notify 唤醒后,会将线程从 WaitSet 移动到 cxq 或 EntryList 中去。需要注意的是,当调用一个锁对象的 wait 或 notify 方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

正文完
 0