文章目录
Synchronized
概念
自增自减字节码指令
我们知道自增自减操作不是原子性的,一行代码它为四条指令
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
iadd // 自增 自减指令是isub
putstatic i // 将修改后的值存入静态变量i中
既然不是原子操作,那么就有可能在最后一步取出操作数栈结果之前进行了线程上下文切换,进而导致线程安全问题
临界区
多个线程对共享资源进行读写操作就会有并发安全问题。
临界区:一段代码对共享资源进行读写操作,这段代码称为临界区
临界资源:共享资源称为临界资源
//临界资源
private static int counter = 0;
public static void increment() {
//临界区
counter++;
}
public static void decrement() {
//临界区
counter--;
}
竞态条件
多个线程对共享资源有竞争,那么也就有竞态条件
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果也无法预测,称为发生了竞态条件
避免临界区中竞态条件发生:
- 阻塞式解决方案:加锁
- 非阻塞式解决方案:CAS原子变量
基本使用
synchronized如果锁对象是类class对象,它是不存在偏向锁的。
private static String lock = "";
public static void increment() {
synchronized (lock){
counter++;
}
}
public static void decrement() {
synchronized (lock) {
counter--;
}
}
接下来的一个执行流程时序图如下所示
原理
在JDK1.5之前,synchronized是基于Monitor机制实现的,其实就是管程。依赖底层操作系统的互斥原语Mutex互斥量,所以就涉及到用户态到内核态的切换。是重量级锁,性能较低。
在JDK1.5之后,添加了锁粗化、锁消除、轻量级锁、偏向锁、自适应自旋等技术减少锁操作的开销。
同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit来实现。
两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
查看synchronized的字节码指令序列
首先是synchronized添加在方法上,通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现
接下来是同步代码块方式,通过monitorenter和monitorexit来实现
Monitor
monitor,翻译是监视器,在操作系统层面叫管程,管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。
在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型
Java语言的内置管程synchronized
java语言内置的管程(synchronized)参考了MESA管程模型,并对它进行了精简。在MESA中条件变量有多个,而java语言内置的管程只有一个条件变量
Monitor机制在Java中的实现
java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于ObjectMonitor
实现,这是 JVM 内部基于 C++ 实现的一套机制。
ObjectMonitor其主要数据结构如下:
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;
}
ObjectMonitor中有三个阻塞队列:_cxq 、_EntryList、_WaitSet
。刚开始多个线程竞争锁,竞争失败的线程进入到_cxq
队列中,它是栈结构。
获取到锁的对象执行后续的业务逻辑,调用等待方法后进入_WaitSet
队列中,被唤醒后根据相应策略进入_cxq
或_EntryList
队列中。
当持有锁对象的线程释放锁后,会根据相应的策略去唤醒_cxq
或_EntryList
队列中的线程。
默认策略(QMode=0)是:_EntryList
队列中不为空,直接从_EntryList
队列中唤醒线程。如果_EntryList
队列为空,则将_cxq
中的元素插入到_EntryList
,并唤醒第一个线程,也就是后来的线程先获取到锁。
对象的内存布局
一个对象是由三部分组成:对象头、实例数据、对其填充
而对象头由三部分组成:Mark Word标记、元数据指针、数组长度
- Mark Work标记:用于标记对象hash值、分代年龄、锁状态标记、线程持有的锁、偏向线程ID、偏向时间戳等。32位机器占4字节,64为机器占8字节
- Klass point指针:指向方法区中类元数据,标识当前对象是哪个类的实例,开启指针压缩后占4字节
- 数组长度:数组对象才有,占四字节
Mark Word是如何记录锁状态的
synchronized加锁是加在对象上的,锁对象是如何记录锁状态的嘞?
锁的信息都是记录在每个对象 对象头的Mark Word中的
32位JVM下的对象头Mark Word结构描述
64位JVM下的对象头Mark Word结构描述
详情:
-
hashCode:对象的hashCode值
-
age:分代年龄
-
biased_lock:偏向锁标记位。
因为无锁和偏向锁都是使用的01锁标志位,这样没办法区分所以就有加了1位来标识是否是偏向锁
-
lock:锁标志位
区分锁的状态,01表示无锁或偏向锁、00表示轻量级锁、10表示重量级锁、11表示对象待GC回收状态
-
JavaThread*:保存持有偏向锁的线程ID,这个不是java中的线程ID,它们不一样
偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
-
epoch:保存偏向时间戳
偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
-
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针
当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针。
-
ptr_to_heavyweight_monitor:重量级锁状态下,会创建一个Monitor对象,指向对象监视器Monitor的指针。
如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。
Mark Word中锁标记枚举
enum {
locked_value = 0, //00 轻量级锁
unlocked_value = 1, //001 无锁
monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
marked_value = 3, //11 GC标记
biased_lock_pattern = 5 //101 偏向锁
}
我们新写一个类,使用JOL工具查看对象的内存布局可以发现,刚开始创建一个对象时,它是001无锁状态,然后用synchronized把这个对象变为锁对象后,它是00轻量级锁状态了。那为什么会跳过偏向锁直接变为了轻量级锁嘞?
偏向锁
什么是偏向锁
偏向锁是一种加锁操作的优化机制。经过研究发现大部分情况下是不存在锁竞争,一直都是一个线程去获取锁,因此为了消除在无竞争情况下重入锁(CAS操作)的开销,而引入了偏向锁。
对于没有竞争的场合,偏向锁有很好的优化效果。
JVM1.6默认开启偏向锁。新创建一个对象,此时给对象的Mark Word中的ThreadID为0,说明该对象处于可偏向但未偏向任何线程,也叫作匿名偏向状态
偏向锁延迟偏向
偏向锁是延迟开启的,这也是为什么我们直接运行一个java类,使用JOL工具查看对象的内存布局时发现对象的锁状态会直接从无锁变为轻量级锁。
之所以有偏向锁延迟的原因是:JVM在启动过程中会有一系列复杂的过程,比如装载配置、系统类初始化等等。在这个过程中会大量使用synchronized来为对象加锁,而且这些锁大多数都不是一个线程用,如果直接使用偏向锁,那么就会存在偏向锁撤销、偏向锁升级等过程。为了减少初始化时间,JVM才默认延迟开启偏向锁。
HotSpot虚拟机默认在启动后4s延迟才会对每个新创建的对象开启偏向锁模式。
相关的JVM启动参数
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking
//启用偏向锁
-XX:+UseBiasedLocking
4s后新创建的对象就开启了偏向锁标识,此时ThreadID还是为0
当有一个线程使用synchronized给这个对象加锁后,就会记录ThreadID
偏向锁状态跟踪
上面锁对象是在4s后创建的一个对象,那如果锁对象是某个类的class对象嘞?
这其实就是从无锁01 --> 轻量级锁00 因为类是class对象是在4s之内创建的。
// 锁对象是class对象
public static void main(String[] args) throws InterruptedException {
// 未加锁
System.out.println(ClassLayout.parseInstance(ObjectTest.class).toPrintable());
// 加锁后
new Thread(()->{
synchronized (ObjectTest.class){
System.out.println(ClassLayout.parseInstance(ObjectTest.class).toPrintable());
}
},"Thread1").start();
}
偏向锁加完锁,并释放后的状态,都是101偏向锁状态
public static void main(String[] args) throws InterruptedException {
//jvm延迟偏向
Thread.sleep(5000);
// 创建时
Object obj = new Test();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
new Thread(()->{
// 加锁后
synchronized (obj){
System.out.println(Thread.currentThread().getName()+"获取锁\n"+ClassLayout.parseInstance(obj).toPrintable());
}
// 释放后
System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
},"Thread1").start();
}
如果线程1释放偏向锁后,线程2又加锁了,此时偏向锁会升级为轻量级锁,也有可能还是偏向锁
public static void main(String[] args) throws InterruptedException {
//jvm延迟偏向
Thread.sleep(5000);
Object obj = new Test();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 线程1先加偏向锁,再释放锁
new Thread(()->{
synchronized (obj){
System.out.println(Thread.currentThread().getName()+"\n"+