引言
一切的最开始都是源自为什么?
- 为什么加了锁
synchronized
关键字,就可以实现同步? synchronized 底层到底做了什么优化?
Java 中的各种锁及锁膨胀?
用户态、内核态与上下文切换到底是什么鬼?
什么叫自旋锁,它与 CAS 的关系?
对象头是什么玩意,什么又是 MarkWord ?
概述
synchronizrd 是开发中解决同步问题中最常见,也是最简单的一种方法。从最开始学习并发编程,我们都知道,只要加上这个 synchronizrd 关键字,就可以很大程度上轻松解决同步问题。相应的,从原理上来讲,其也是比较重的一种操作,特别是 jdk1.5 时候,相比 JUC 中的 Lock 锁,一定程度上逊色不少。但随着jdk1.6对 synchronized 的优化后,synchronizrd 并不会显得那么重,相比使用 Lock 而言,其的性能大多数情况下也可以接近 Lock 。
本文的主旨就是对 synchronized 的原理进行探秘,从而完成对各种锁的了解与学习。
synchronizrd 常用的作用有三个:
原子性
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行;
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值;
有序性
防止编译器和处理对指令进行重排序,即也就是抑制指令重排序,;
解析
synchronzied
在 jvm
里的实现都是基于进入和退出 Monitor
对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过 成对 的 MonitorEnter
和 MonitorExit
指令来实现。具体如下图对比:
示例代码:
字节码对比:
对同步方法 ,从上图字节码来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来实现,而是直接在方法中增加了 synchronzied 修饰。更底层实现上而言,其常量池中多了 ACC_SYNCHRONIZED 标识符,JVM 就是根据该标识符来实现方法的同步:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor ,获取成功之后才能执行方法体,方法执行完后再释放 monitor 。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。
对于 同步代码块 而言,synchronzied 的底层实现中,MonitorEnter 指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象 Monitor 的所有权,即尝试获得该对象的锁,而 monitorExit 指令则插入在方法结束处和异常处,JVM 保证每个 MonitorEnter 必须有对应的 MonitorExit 。
在上面,说到了 synchronized 的在字节码上的实现,那对于虚拟机而言,synchronzied 锁的标志到底放哪了呢?说到这个问题,我们不得不提 对象头 这个概念。
对象头
什么是对象头,对象头干了什么?
如果看过垃圾回收机制,那么可能知道这个玩意。对象头其就相当于一个名片,它包含了对象的一个基本信息,如下图所示:
注意:对象头中不一定包含数组长度,如果这个对象不是数组 ✋
整个对象头由两个部分组成,分别是 KlassPoint
与 Mark Word
。
KlassPoint
当我们 new 出一个类时,虚拟机如何得知它是哪个类呢, 这时候就是通过上述 KlassPoint
,其指向了类元数据 (mteaData)
的信息。
元数据
在计算机中,有各种 [元] 数据。比如文件有元数据,网页有元标签。元
这个说法来自希腊语,表示关于。所以 文件中的元数据,即为关于文件的数据,类的元数据即为类信息的一个原始标签。也即为描述这个类的信息
Mark Word
用于存储不同状态信息,是会随着时间点而改变。一般而言默认数据是存储对象的 HashCode
等信息,而我们本篇的主题 synchronzied
正是在其里存储。如下图所示
Markword的信息会随着时间不断改变,比如发生gc时,内部gc 标记为null。
而我们本篇的主题 synchronized 的 锁状态 也存在与 MarkWord 中,在对象运行变化的过程中,锁的状态存在4种变化状态,即 无锁状态 、偏向锁状态 、轻量级锁状态 、重量级锁状态 。它会随着竞争情况逐渐升级,锁可以升级但不能降级,主要目的是为了提高获得锁和释放锁的效率。
那为什么要存在着几种🔐呢?或者还没看明白缘由?请接着下面继续看?👇
上下文切换
在jdk1.6之后,synchronizrd 得到了优化,而添加各种锁的目的都是为了避免直接加锁而导致的上下文切换从而引发的耗时浪费。
如果不了解上下文切换,这样说可能听着有点懵,我们先从基础讲一下:
什么是上下文切换?
1.在单处理器时期,操作系统就能处理多线程并发任务,处理器给每个线程分配CPU时间片,线程在CPU时间片内执行任务
- CPU时间片是CPU分配给每个线程执行的时间段,一般为几十毫秒
2.时间片决定了一个线程可以连续占用处理器运行的时长
- 当一个线程的时间片用完,或者因自身原因被迫暂停运行,此时另一个线程会被操作系统选中来占用处理器
- 上下文切换(Context Switch):一个线程被暂停剥夺使用权,另一个线程被选中开始或者继续运行的过程
- 切出:一个线程被剥夺处理器的使用权而被暂停运行
- 切入:一个线程被选中占用处理器开始运行或者继续运行
- 切出切入的过程中,操作系统需要保存和恢复相应的进度信息,这个进度信息就是*上下文*
3. 上下文的内容
- 寄存器的存储内容:CPU寄存器负责存储已经、正在和将要执行的任务
- 程序计数器存储的指令内容:程序计数器负责存储CPU正在执行的指令位置、即将执行的下一条指令的位置
4.当CPU数量远远不止1个的情况下,操作系统将CPU轮流分配给线程任务,此时的上下文切换会变得更加频繁
- 并且存在跨CPU的上下文切换,更加昂贵
所以,当我们某个资源使用 synchronizrd 进行加锁时:
- 当线程A获取了锁,线程B在获取时将会被阻塞,也即是
BLOCKED
状态,此时线程B暂停被操作系统 切出 ,操作系统会保存此时的上下文; - 当线程A释放了锁,此时假设线程B获取到了锁,线程B 从
BLOCKED
进入RUNNABLE
状态,即线程重新唤醒,此时线程将获取上次操作系统保存的上下文继续执行。
上述的过程中线程B执行了 两次 上下文切换,每一次上下文切换的过程为 3~5微秒 ,而cpu执行一条指令只需要 0.6ns ,所以如果加锁后只是执行几条普通指令,如某个变量的自增或者其他,那么上下文切换将对性能产生极大影响,所以在jdk1.6以后,synchronizrd 得到了优化,新增了几种锁,以及不同情况下的状态变化,以避免直接重量级锁产生的性能损耗。