并发编程(3)-synchronized的实现原理

并发编程学习目录

并发编程(1)-java中的6中线程状态
并发编程(2)-怎么中断线程?

一、synchronized锁是如何存储的

从synchronized(obj)的语法可以看出,似乎和这个对象有关系。我们以jvm内存中是如何存储作为切入点,去看看对象里面有什么特性可以实现锁

1.对象在内存中的布局

在hotspot虚拟机中,对象的布局可以分为3个部分:对象头(Header)、实例数据(instance Data)、对其填充(Padding)
在这里插入图片描述

2.jvm源码实现

当我们在java代码中,使用new关键词创建一个实例对象时,(hotspot虚拟机)JVM 层面实际上会创建一个instanceOopDesc对象。
在这里插入图片描述
instanceOopDesc定在hotspot源码中的instanceOop.hpp文件中,另外arrayOopDesc定于在对应的arrayOop.hpp

class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }

  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

从 instanceOopDesc 代码中可以看到 instanceOopDesc继承自 oopDesc,oopDesc 的定义载 Hotspot 源码中的oop.hpp 文件中

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

在普通实例对象中,oopDesc 的定义包含两个成员,分别是 _mark 和 _metadata,_mark 表示对象标记、属于 markOop 类型,也就是接下来要讲解的 Mark World,它记录了对象和锁有关的信息,_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中 Klass 表示普通指针、_compressed_klass 表示压缩类指针

3.markword

在 Hotspot 中,markOop 的定义在 markOop.hpp 文件中,代码如下

  enum { age_bits                 = 4, // 分代年龄
         lock_bits                = 2, // 锁的标识
         biased_lock_bits         = 1, // 是否为偏向锁
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits, //对象的hash值
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0), 
         epoch_bits               = 2 //偏向锁的时间戳
  };

markword记录了对象和锁的相关信息,当某个对象被synchronized关键字当成同步锁时,围绕锁的一系列操作都和markword有关。Mark Word 在 32 位虚拟机的长度是 32bit、在 64 位虚拟机的长度是64bit。
Mark Word 里面存储的数据会随着锁标志位的变化而变化,Mark Word 可能变化为存储以下 5 中情况:
在这里插入图片描述

4.为什么任何对象都可以实现锁
  1. 首先,Java 中的每个对象都派生自 Object 类,而每个Java Object 在 JVM 内部都有一个 native 的 C++对象oop/oopDesc 进行对应。
  2. 线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。在 hotspot 源码的markOop.hpp 文件中,可以看到下面这段代码。
  ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
  }

多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识,上面的代码中ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系

二、synchronized锁升级

经调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获取。所以在jdk1.6之后做了一些优化,为了减少获取锁和释放锁,线程的阻塞唤醒所带来的性能开销,引入了偏向锁、轻量级锁的概念。synchronized锁的四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁。锁的竞争激烈的程度是从低到高不断升级的。

1.偏向锁的获取和撤销

在这里插入图片描述
参数

-XX:+/-UseBiasedLocking 启动/禁用偏向锁,默认虚拟机启动4秒后启动偏向锁

-XX:BiasedLockingStartupDelay 虚拟机启动后,立刻启动偏向锁

偏向锁的获取
  1. 首先获取锁 对象的 Markword,判断是否处于可偏向状态。(biased_lock=0、且 ThreadId 为空)
  2. 如果是可偏向状态,则通过 CAS 操作,把当前线程的 ID写入到 MarkWord
    a) 如果 cas 成功,那么 markword 就会变成这样。表示已经获得了锁对象的偏向锁,接着执行同步代码块
    b) 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
  3. 如果是已偏向状态,需要检查 markword 中存储的ThreadID 是否等于当前线程的 ThreadID
    a) 如果相等,不需要再次获得锁,可直接执行同步代码块
    b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
偏向锁的撤销

对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程
有两种情况:

  1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程
  2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块
2.轻量级锁堆获取和撤销

在这里插入图片描述
锁升级为轻量级锁之后,对象的 Markword 也会进行相应的的变化。升级为轻量级锁的过程:

  1. 线程在自己的栈桢中创建锁记录 LockRecord。
  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
  3. 将锁记录中的 Owner 指针指向锁对象。
  4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。
    在这里插入图片描述
    获取轻量级锁之后
    在这里插入图片描述
自旋锁

轻量级锁在加锁过程中,用到了自旋锁,所谓自旋,就是指当有另外一个线程来竞争锁时,这个线
程会在原地循环等待
,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的 for 循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。所以通过看似无异议的循环反而能提升锁的性能。但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认情况下自旋的次数是 10 次,可以通过 preBlockSpin 来修改
在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

轻量级锁的释放

轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

3.重量级锁

每一个 JAVA 对象都会与一个监视器 monitor 关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能
在这里插入图片描述
任意线程对 Object(Object 由 synchronized 保护)的访问,首先要获得 Object 的监视器。如果获取失败,线程进入同步队列,线程状态变为 BLOCKED。当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值