资讯专栏INFORMATION COLUMN

Java 中关于锁的一些理解

Yumenokanata / 2926人阅读

摘要:每一个被锁住的对象都会和一个关联对象头的中的指向的起始地址,同时中有一个字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

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的实现原理与应用)

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

偏向锁
偏向锁是锁的级别中最低的锁,举个例子: 在此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 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带来的开销基本可以忽略;
当其他线程进入同步块中时,发现偏向线程不是当前线程,则进入到撤销偏向锁的逻辑,当达到全局安全点时,锁开始膨胀为轻量级锁,原来的线程仍然持有锁,如果发现偏向线程挂了,那么就把对象的头改为无锁状态,锁膨胀

轻量锁

当锁膨胀为轻量级锁时,首先判断是否有线程持有锁(判断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);
}

重量锁

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74342.html

相关文章

  • 深入理解volatile类型——从Java虚拟机内存模型角度

    摘要:本文从内存模型角度,探讨的实现原理。通过共享内存或者消息通知这两种方法,可以实现通信或同步。基于共享内存的线程通信是隐式的,线程同步是显式的而基于消息通知的线程通信是显式的,线程同步是隐式的。锁规则锁的解锁,于于锁的获取或加锁。 一、前言 在java多线程编程中,volatile可以用来定义轻量级的共享变量,它比synchronized的使用成本更低,因为它不会引起线程上下文的切换和调...

    mushang 评论0 收藏0
  • Java 虚拟机对锁优化所做的努力

    摘要:自选锁锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会在做最后的努力自选锁。 showImg(https://segmentfault.com/img/remote/1460000016159660?w=500&h=333); 作为一款公用平台,JDK 本身也为并发程序的性能绞尽脑汁,在 JDK 内部也想尽一切办法提供并发时的系统吞吐量。这里,我将向大家简单介绍几种 J...

    ralap 评论0 收藏0
  • Android并发编程 多线程与锁

    摘要:本篇文章介绍多线程与锁。阻塞表示线程阻塞于锁。等待进入该状态的线程需要等待其他线程做出一些特定动作通知或中断。终止表示该线程已经执行完毕。实际中无法保证达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。 该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的。 前言 前一篇Android并发编程开篇呢...

    Forest10 评论0 收藏0
  • BATJ都爱问的多线程面试题

    摘要:今天给大家总结一下,面试中出镜率很高的几个多线程面试题,希望对大家学习和面试都能有所帮助。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。使用可以禁止的指令重排,保证在多线程环境下也能正常运行。 下面最近发的一些并发编程的文章汇总,通过阅读这些文章大家再看大厂面试中的并发编程问题就没有那么头疼了。今天给大家总结一下,面试中出镜率很高的几个多线...

    高胜山 评论0 收藏0
  • 线程间的同步与通信(4)——Lock 和 Condtion

    摘要:为了拓展同步代码块中的监视器锁,开始,出现了接口,它实现了可定时可轮询与可中断的锁获取操作,公平队列,以及非块结构的锁。 前言 系列文章目录 前面几篇我们学习了synchronized同步代码块,了解了java的内置锁,并学习了监视器锁的wait/notify机制。在大多数情况下,内置锁都能很好的工作,但它在功能上存在一些局限性,例如无法实现非阻塞结构的加锁规则等。为了拓展同步代...

    Aceyclee 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<