Synchronized关键字详解
synchronized的基本认识
- 数据不安全性的本质在于:共享数据存在并发操作
- jdk1.6对synchronized进行了优化,引入了偏向锁和轻量级锁的概念。
- synchronized有三种加锁的方式,不同的修饰类型,代表了锁的控制粒度
- 修饰实例方法,作用于当前对象实例
- 修饰静态方法,作用于当前类对象
- 修饰代码块,作用于当前代码块,进入同步代码块前要获得给定对象的锁
- HostSpot虚拟机中,对象的存储分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
- 互斥不是根据修饰的是不是同一个类的方法判断,而是根据是不是同一个锁对象判断,是同一个锁对象才会互斥
漫谈对象存储(仅针对于hotspot虚拟机)
- new创建一个对象的时候JVM层面实际上会创建一个instanceOopDesc对象,instanceOopDesc定义在instanceOop.hpp文件中,instanceOopDesc继承自oopDesc,oopDesc定义在Hotspot源码中的oop.hpp文件中
- 普通实例对象中,oopDesc的定义包含两个成员,分别是_mark和_metadata
- _mark表示对象标记,也就是MarkWord,记录了对象和锁的有关信息
- _metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针、_compressed_Klass表示压缩类指针
- MarkWord的定义【32位虚拟机】:
- MarkWord的存储情况【32位虚拟机】
锁的存储
- 为什么任何对象都可以实现锁?
- java中的每个对象都继承自Object类,而每个Object在JVM内部都有一个native的C++对象oop/oopDesc进行对应
- 线程在获取锁的时候,实际上就是获得一个监视器对象,monitor可以认为是一个同步对象,所有的java对象是天生携带monitor。多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识。
synchronized锁的升级
在synchronized中,锁存在四种状态:无锁、偏向锁、轻量级锁、重量级锁。
偏向锁
- 偏向锁的基本原理:当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁,而是直接比较对象头里面是否存储了当前线程的偏向锁。如果相等则表示偏向锁是偏向于当前线程的,就不再尝试获得锁了。
- 偏向锁的获取:
- 获得锁对象的MarkWord,判断是否处于可偏向状态(biased_lock_bits=1,且ThreadId为空)
- 如果是可偏向状态,则通过CAS操作,把当前线程的ID写入MarkWord
- 如果CAS成功,表示当前线程获得了偏向锁,并执行同步代码
- 如果CAS失败,说明存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才执行
- 如果是已偏向状态(偏向锁不会主动释放锁),检查MarkWord中存储的ThreadID是否等于当前线程的ThreadID
- 如果相等,直接执行同步代码
- 如果不相等,就会进行偏向锁撤销(或重新偏向,或锁升级)
- 偏向锁的撤销:偏向锁的撤销并不是把对象恢复到无锁可偏向状态,因为偏向锁并不存在锁释放的概念
- 原获得偏向锁的线程已经退出了临界区(同步代码块执行完),那么会将锁对象设置成无锁状态并重新偏向
- 如果未退出临界区,则暂停当前获得锁的线程,升级为轻量级锁。
- 其他:偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;(在高并发场景下,有时开启偏向锁反而会提高获取锁资源的消耗)
轻量级锁
- 升级轻量级锁的过程
- 线程在自己的栈帧中创建锁记录LockRecord
- 将锁对象的对象头中的MarkWord复制到刚刚创建的LockRecord中
- 将LockRecord中的Owner指针指向锁对象
- 将锁对象的对象头的MarkWord替换为 指向LockRecord的指针
- 自旋锁:轻量级锁在加锁的过程中用到了自旋锁,当另外线程竞争锁时会自旋原地等待,而不是把线程阻塞。自旋也必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,反而会消耗过多CPU资源。默认情况下自旋次数为10次,可以通过-XX:PreBlockSpin=10来修改。
- 自适应自旋锁:在jdk1.6之后引入了自适应自旋锁,这种自旋锁的自旋次数不是固定不变的。比如某个锁,自旋很少获得成功过,那么以后自旋次数会很少或者直接阻塞线程。
- 轻量级锁的解锁:其实就是获得锁的逆向逻辑,通过CAS操作把线程栈帧中的LockRecord替换回到锁对象的MarkWord中,如果成功表示没有竞争,失败则会膨胀为重量级锁。
重量级锁
重量级锁的进入与退出:
当轻量级锁膨胀到重量级锁之后,就意味着线程只能被挂起阻塞来等待被唤醒了
- 编写测试文件:
- 执行javac命令,生成class文件
- 执行javap -c命令,查看字节码
- 分析:
- 每一个Java对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized修饰的同步代码块时,该线程得先获取synchronized修饰的对象对应的monitor
- monitorenter表示去获得一个对象监视器,monitorexit表示释放monitor的所有权,使得其他被阻塞的线程可以去尝试获得这个监视器
- monitor依赖操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态和内核态之间来回切换,严重影响了锁的性能。
重量级锁加锁的基本流程: