1. 锁的存储
要实现多线程的互斥特性 那么这把锁必须要有一个东西来表示 , 比如获得锁是什么状态, 无锁是什么状态 以及这个锁要对多少个线程共享等 那么锁是如何存储的?
在Hotspot虚拟机中, 对象在内存中的存储方式可以分为 三个区域 : 对象头(Header),实例数据 (Instance Date),对齐填充(Padding)
当我们new创建一个对象实例的时候,jvm 层实际会创建一个instanceOopDesc 对象 。Hotspot 虚拟机采用 OOP-Klass 模型来描述 Java 对象实 例,OOP(Ordinary Object Point)指的是普通对象指针,Klass 用来描述对象实例的具体类型。Hotspot 采用instanceOopDesc 和 arrayOopDesc 来描述对象头,arrayOopDesc 对象用来描述数组类型
instanceOopDesc 的 定 义 在 Hotspot 源 码 中 的instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对 应 arrayOop.hpp
从 instanceOopDesc 代码中可以看到 instanceOopDesc继承自 oopDesc,oopDesc 的定义载 Hotspot 源码中的oop.hpp 文件中
在普通实例对象中,oopDesc 的定义包含两个成员,分别 是 _mark 和 _metadata
_mark 表示对象标记、属于 markOop 类型,也就是接下来 要讲解的 Mark World,它记录了对象和锁有关的信息_metadata 表示类元信息,类元信息存储的是对象指向它 的类元数据(Klass)的首地址,其中 Klass 表示普通指针、_compressed_klass 表示压缩类指针MarkWord
在 Hotspot 中,markOop 的定义在 markOop.hpp 文件中,代码如下
markword用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,当某个对象被synchroized 关键字当成同步锁时,那么围绕这这个锁的一系列操作都和markword 有关。mark word 在32位系统中长度为32bit ,64位则是64bit
2 为什么任何对象都可以实现锁
- java中的每一个对象都是派生自Object 类,在java Object 在jvm 内部 都有一个native 的c++对象opp/oppDesc与之对应
- 线程在获取锁的时候 实际上就是获取监视器对象 monitor ,monitor可以认为是一个同步对象, 所有的java对象天生就携带monitor在 hotspot 源码的markOop.hpp 文件中,可以看到下面这段代码。
多个线程访问同步代码块时,相当于去争抢对象监视器 修改对象中的锁标识,上面的代码中 ObjectMonitor 这个 对象和线程争抢锁的逻辑有密切的关系
3 synchronized 锁升级
锁的四种状态 : 无锁,偏向锁, 轻量级锁 , 重量级锁
- 偏向锁 : 当一个线程访问了加了锁的同步代码块时,会在对象头中添加线程id , 后续这个线程在进入或者退出这段同步代码块的时候就不需要加锁和释放锁了 而是 直接比较当前对象的对象头中是否存储了当前线程的id , 如果存储 表示当前偏向锁是偏向这个线程的,就不需要在尝试获得锁了
偏向锁的获取:
1 首先判断对象头中的markword 是否处于可偏向状态( ThreadId 为空 , biased_lock=1)
2 如果是可偏向状态, 则此线程进行cas操作 将该线程的线程id 写入对象头中的markword ,如果写入成功, 表示该线程成功获得偏向锁,并可以执行同步代码块, 如果写入失败, 则表示此时已有线程获正在获取偏向锁 ,这种情况就说明了存在锁竞争,需要撤销掉获得偏向锁的线程id ,并且将偏向锁升级成轻量级锁 ,这个 操作需要等到全局安全点,也就是没有线程在执行字 节码)才能执行;
3 如果是已偏向状态, 则检查markword 中的线程id 是否等于 该线程id 如果相等 ,不需要再获得锁 ,直接访问同步代码块 ; 如果不相等,则将markword 中的线程id撤销 并将锁升级为轻量级锁
偏向锁的撤销:
对原持有偏向锁的线程进行撤销时 ,原获得偏向锁的线程 有两种情况:
1 原获得偏向锁的线程如果已经退出临界区了 表示同步代码块已经执行完了, 那么 这个时候会把对象头中的锁设置成无锁状态并且争抢的线程可以基于cas 操作重新获得偏向锁
2 元获得偏向锁的线程如果同步代码块还没有执行完,那么会把原获得偏向锁的线程 升级为轻量级锁 , 然后继续访问同步代码块
- 轻量级锁
轻量级锁获取
锁升级为轻量级锁 对应的markword 也会发生改变。升级轻量级锁的过程如下:
-
1线程在自己的栈桢中创建锁记录 LockRecord。
-
2将锁对象的对象头中的 MarkWord 复制到线程的刚刚创建的锁记录中。
-
3将锁记录中的 Owner 指针指向锁对象
-
4将锁对象的对象头的 MarkWord 替换为指向锁记录的指针
自旋锁
轻量级锁在加锁过程中,用到了自旋锁 所谓自旋,就是指当有另外一个线程来竞争锁时,这个线 程会在原地循环等待,而不是把该线程给阻塞,直到那个 获得锁的线程释放锁之后,这个线程就可以马上获得锁的。 注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于 在执行一个啥也没有的 for 循环。 所以,轻量级锁适用于那些同步代码块执行的很快的场景, 这样,线程原地等待很短的时间就能够获得锁了。 自旋锁的使用,其实也是有一定的概率背景,
在大部分同 步代码块执行的时间都是很短的。所以通过看似无异议的 循环反而能提升锁的性能。 但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而 会消耗 CPU 资源。默认情况下自旋的次数是 10 次;在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋 的次数不是固定不变的,而是根据前一次在同一个锁上自 旋的时间以及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并 且持有锁的线程正在运行中,那么虚拟机就会认为这次自 旋也是很有可能再次成功,进而它将允许自旋等待持续相 对更长的时间。如果对于某个锁,自旋很少成功获得过, 那在以后尝试获取这个锁时将可能省略掉自旋过程,直接 阻塞线程,避免浪费处理器资源
轻量级锁的解锁
轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,如果成功表示没有竞争。如果失败,表示 当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁
- 重量级锁
当轻量级锁膨胀到重量级锁 ,意味着线程只能被挂起阻塞 等待被唤醒了
加了同步代码块以后,在字节码中会看到一个monitorenter 和 monitorexit。
每一个 JAVA 对象都会与一个监视器 monitor 关联,我们 可以把它理解成为一把锁,当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先 获取到 synchronized 修饰的对象对应的 monitor。monitorenter 表示去获得一个对象监视器。monitorexit 表 示释放 monitor 监视器的所有权,使得其他被阻塞的线程 可以尝试去获得这个监视器monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线 程被阻塞后便进入内核(Linux)调度状态,这个会导致系 统在用户态与内核态之间来回切换,严重影响锁的性能
任意线程对 Object(Object 由 synchronized 保护)的访 问,首先要获得 Object 的监视器。如果获取失败,线程进 入同步队列,线程状态变为 BLOCKED。当访问 Object 的 前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻 塞在同步队列中的线程,使其重新尝试对监视器的获取