临界区
什么是临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源
- 一个程序运行多个线程本身没有问题
- 多个线程读共享资源也没有问题
- 多个线程对共享资源读写操作时会发生指令交错,就会出问题
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同导致结果无法预测,称之为发生了竞态条件。
为了避免竞态条件发生,有多种手段可以达到目的:
阻塞式的解决方案:synchronized、Lock
非阻塞式:原子变量
synchronized的使用
加锁方式
synchronized同步块是Java提供的一种原子性内置锁,Java中每个对象都可以把它当做一个同步锁来使用,这些Java内置的使用者看不到的锁称为内置锁,也叫作监视器锁
加锁方式
分类 | 具体分类 | 被锁的对象 | 伪代码 |
---|---|---|---|
方法 | 实例方法 | 类的实例对象 | public synchronized void method (){…} |
方法 | 静态方法 | 类对象 | public static synchronized void methis (){…} |
代码块 | 实例对象 | 类的实例对象 | synchronized (this){…} |
代码块 | class对象 | 类对象 | synchronized (TestDemo.class){…} |
代码块 | 任意实例对象 | 实例对象Object | synchronized (newObject){…} |
本质:方法上(acc_synchronized) 代码块(monitorenter monitorexit)
解决之前的共享问题
public static void increment() {
synchronized (lock){
counter++;
}
}
synchronized底层原理
synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥源语Mutex(互斥量),它是一个重量级锁,性能较低。当然内置锁在1.5之后版本做了重大的优化,来减少所得开销,内置锁的并发性能已经基本与Lock持平,如:
- 锁粗化(Lock Coarsening)
- 锁消除(Lock Elimination)
- 轻量级锁(Lightwright Locking)
- 偏向锁(Biased Locking)
- 自适应自旋(Adaaptive Spinning)
Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步:monitor
同步方法是通过方法中access_flags中设置ACC_SYNCHRONIED标志来实现;同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行时JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大的影响。
查看sychronized的字节码指令序列
Monitor(管程/监视器)
管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发,synchronized和wait()、notify()、notifyAll()都是Java中管程技术的组成部分
MESA模型
最广泛使用的管程模型
管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。
核心概念:入口等待队列(互斥) 多个条件队列(同步 阻塞唤醒机制)
wait()的正确使用方式
对于MESA来说,有一个编程范式:
while(条件不满足) {
wait();
}
JVM层面对管程的实现
参照synchroized,组成部分如下:
ObjectMonitor
cxq(cas owner)
waitset(wait/notify,notifyAll)
Java层面对管程的实现
AQS抽象层:
同步等待队列(cas volatile int state) 入队,出队 加锁,解锁
条件等待队列(Condition await/signal,signalAll) 入队,出队
Java语言内置管程synchronized
Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量
Monitor机制在Java中的实现
java.lang.Object类定义了wait(),notify(),notifyAll()方法,这些方法的具体实现,依赖于ObjectMonitor,这是JVM基于C++实现的一套机制(hotspot源码ObjectMonitor.hpp)
ObjectMonitor() {
_header = NULL; //对象头 markOop
_count = 0;
_waiters = 0,
_recursions = 0; // 锁的重入次数
_object = NULL; //存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
FreeNext = NULL ;
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失 败的线程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略(
QMode=0)是:如果EntryList为空,则将
cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取
锁。_EntryList不为空,直接从_EntryList中唤醒线程。
因此:获取锁是非公平的
对象的内存布局
Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头详解
详情参考 JVM-2-JVM内存模型与优化
Mark Wrok是如何记录锁状态的
Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。简单点理解就是:MarkWord 结构搞得这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。
Mark Word的结构
- hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
- age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
- biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
- lock: 锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
- JavaThread:保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
Mark Word中锁标记枚举
关于锁的总结
偏向锁:不存在竞争,偏向某个线程 thread1后续进入同步块的逻辑没有加锁解锁的开销,不存在 无锁->轻量级锁
轻量级锁:线程间存在轻微的竞争(线程交替执行,临界区罗技简单) CAS获取锁(无自旋),失败膨胀
重量级锁:多线程竞争激烈的场景,膨胀期间创建一个monitor对象,CAS自旋,阻塞
利用JOL跟踪锁标记变化
偏向锁
偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由统一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的的优化效果。
当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的ThreadId为0,说明此时处于可偏向但未偏向任何线程,也叫作匿名偏向状态。
偏向锁延迟偏向
偏向锁模式存在偏向延迟机制:HotSpot虚拟机在启动后有个4s的延迟才会对每个新建的对象开启偏向锁模式。
偏向锁撤销
HashCode
一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashCode的
- 轻量级锁会在锁记录中记录hashCode
- 重量级锁会在Monitor中记录hashCode
- 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级为轻量级锁
- 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级为重量锁
wait/notify
偏向锁状态执行obj.notify()会升级为轻量级锁,调用wait会升级为重量级锁
轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时MarkWord的结构也变为轻量级锁的结构。轻量级锁适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。
轻量级锁是否可以降级为偏向锁?
锁升级
偏向锁升级为轻量级锁
轻量级锁膨胀为重量级锁
重量级锁释放后变为无锁,此时有新线程来调用同步块,会获取什么锁?
总结:锁对象状态转换
synchronized锁优化
批量重偏向/批量撤销
- 批量重偏向和批量撤销是针对类的优化,和对象无关
- 偏向锁重偏向一次之后不可再次重偏向
- 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重问题,剥夺了该类的新实例对象使用偏向锁的权利
- 撤销偏向锁有性能问题,因此需要批量撤销,达到阈值后剥夺偏向锁权利
自旋优化
针对重量级锁竞争,使用自适应自旋来进行优化
自旋的目的是为了减少线程挂起的次数(设计用户态和内核态的切换,这才是重量级锁最大的开销)
锁粗化/锁消除
锁粗化:对一个对象反复加锁及解锁,甚至加锁操作是在循环体中出现的,例如:StringBuffer.append()
锁消除:JIT编译期间删除不必要的加锁操作(逃逸分析)