3. 第三个概念集合——对synchronized锁,原理,概念,API调用的集合,以及锁升级,在使用API的时候,发生了什么事情
synchronized的使用背景和基础概念
这个在JavaSE的时候就有介绍。
就是因为线程多线程,在Java中的时候,数据不安全。
线程访问数据,到线程的内存里
不是CPU的级别的内存
会导致超卖的问题。
这个时候,官方已经解决这个问题,就是创造了这个synchronized关键字。
这个时候使用synchronized这个锁,就可以保证,
同一时间,只会有一个线程访问内存里的数据。
我们基础层面的掌握,就是掌握一下,什么时候使用这个关键字,怎么使用这个关键字。
补充说明:
这里的“线程的内存”指的是Java内存模型(JMM)中定义的工作内存(线程私有栈内存中的变量副本),而不是CPU缓存。每个线程从主内存拷贝共享变量到自己的工作内存中操作,操作完成后再写回主内存,这个过程如果不加控制就会导致可见性问题。
“超卖问题”典型场景如:两个线程同时读取到库存=1,各自在工作内存中减到0,再先后写回主内存,最终结果还是0,但实际卖出了两份。synchronized通过互斥访问解决了这种并发修改的不安全问题。
synchronized在代码使用层面需要了解的概念和知识
synchronized这个关键字,可以修饰什么,怎么使用
补充说明——synchronized的三种使用方式:
修饰实例方法
publicsynchronizedvoidmethod(){// 同步代码}锁对象:当前实例对象(this)
修饰静态方法
publicstaticsynchronizedvoidstaticMethod(){// 同步代码}锁对象:当前类的Class对象(如 MyClass.class)
修饰代码块
synchronized(lockObject){// 同步代码}锁对象:括号中指定的任意对象
使用选择建议:
- 方法内只有少量代码需要同步 → 用同步代码块,减少锁持有时间,提高并发度
- 整个方法都需要同步 → 可以修饰方法,代码更简洁
- 控制粒度精细(如一个方法内对不同资源分别加锁) → 用不同的锁对象分开控制
synchronized在代码原理层面需要了解的概念和知识
我们看一下synchronized的例子。
// 同步代码块的常见用法privatefinalObjectlock=newObject();// 专门的锁对象publicvoidsomeMethod(){synchronized(lock){// 需要同步的代码}}这个要锁一个对象,为什么是锁一个对象?
我们知道,对象在JVM里,可以分为:
- 对象头(Header)
- 实例数据(Instance Data)
- 填充数据(Padding)
对象头里面有丰富的信息,包括:
- Mark Word(标记字段)
- Klass Pointer(类型指针)
- 数组长度(如果对象是数组)
Mark Word 中存储了与锁相关的核心信息:
- 哈希码(hashCode)
- GC分代年龄(age)
- 锁状态标志位(01、00、10、11)
- 指向线程ID的指针(偏向锁模式下)
- 指向重量级锁(monitor)的指针
补充说明:为什么锁的是一个对象?
在JVM设计中,每个Java对象都可以天然地作为锁,根本原因就在于对象头的Mark Word中包含了锁状态信息。当线程想要进入同步块时,JVM会检查目标对象的对象头:
- 判断当前锁状态(无锁、偏向锁、轻量级锁、重量级锁)
- 根据状态决定采用哪种锁机制
- 通过CAS或monitor机制竞争锁
- 成功则修改对象头的锁状态记录,失败则阻塞或自旋等待
锁对象的选择原则:
- 多个线程竞争的必须是同一个对象(同一内存地址的同一个实例)
- 如果是不同对象,就是不同的锁,不会互斥
- 常用做法:使用
private final Object lock = new Object()可以防止锁对象被意外修改
锁升级(Lock Escalation / Biased Locking → Lightweight Lock → Heavyweight Lock)
JVM锁优化的核心机制:从JDK 6开始,synchronized引入了锁升级机制,避免一上来就使用重量级锁(操作系统的互斥量,需要用户态到内核态切换,开销大),而是根据竞争激烈程度动态调整锁的状态。
锁的状态(级别从低到高):
- 无锁→ 2.偏向锁→ 3.轻量级锁→ 4.重量级锁
锁可以升级,但不能降级(偏向锁→轻量级→重量级单向)
(1)偏向锁(Biased Locking)
- 场景:锁被同一个线程反复获取,没有竞争
- 原理:第一次获取锁时,在对象头的Mark Word中记录该线程ID。后续该线程再进入同步块时,只需检查线程ID是否为当前线程,无需CAS操作
- 撤销:当另一个线程来竞争时,偏向锁需要撤销(在全局安全点执行),可能升级为轻量级锁
(2)轻量级锁(Lightweight Locking)
- 场景:少量线程交替持有锁,没有实际阻塞
- 原理:线程在执行同步块前,在当前线程栈帧中创建锁记录(Lock Record),通过CAS将对象头的Mark Word替换为指向锁记录的指针。成功则获取锁,失败则说明有竞争
- 特点:未获取锁的线程会自旋(CAS重试),不自旋失败后才膨胀为重量级锁
(3)重量级锁(Heavyweight Locking)
- 场景:多条线程竞争激烈,自旋等待时间过长
- 原理:锁膨胀为重量级锁后,对象头的Mark Word指向一个monitor对象(ObjectMonitor)
- 特点:未获取到锁的线程会阻塞(进入等待队列),由操作系统调度,涉及用户态到内核态切换,开销最大
在使用synchronized API的时候,发生了什么事情(锁升级过程全流程)
用一个简单例子说明:
privatefinalObjectlock=newObject();publicvoidmethod(){synchronized(lock){// 临界区代码}}执行流程:
创建对象lock时
对象头Mark Word处于无锁状态(锁标志位01,偏向锁标志位0,表示暂时未启动偏向)第一次线程T1进入synchronized块
- JVM检查偏向锁是否启用(JVM默认启动偏向锁,但有延迟,默认4秒)
- 如果启用并且没有竞争 → 进入偏向锁模式
Mark Word中记录T1的线程ID,锁标志位改为偏向锁(01,偏向标志位=1) - 此后T1再次进入该同步块,只需检查线程ID是否匹配,不再CAS
线程T2来竞争锁(此时锁还在T1手中)
- T2发现Mark Word中已经有T1的线程ID → 偏向锁需要撤销
- 在全局安全点(SafePoint)暂停T1线程,检查T1是否还存活
- 如果T1已经退出同步块 → 偏向锁撤销,回到无锁状态,然后T2获得偏向锁
- 如果T1仍在同步块内 → 偏向锁升级为轻量级锁
轻量级锁的竞争
- T1和T2都在各自线程栈帧中创建锁记录(Lock Record)
- 通过CAS尝试将对象头的Mark Word指向自己的锁记录
- 成功者(T1)持有锁,失败者(T2)自旋(默认自旋次数或自适应自旋)
自旋一定次数后仍未获得锁→ 锁膨胀为重量级锁
- JVM为lock对象分配一个ObjectMonitor(包含EntryList、WaitSet等)
- 未获得锁的线程(T2等)挂起,进入操作系统的阻塞状态
- 后续所有线程获取锁都需要通过monitor的enter/exit机制
API调用层面的统一性:
无论锁处于什么级别,synchronized代码块在Java源码层面写法完全一样。JVM内部会根据运行时竞争情况自动选择锁策略,对程序员透明。
补充:JVM参数控制锁升级(了解即可)
| 参数 | 作用 |
|---|---|
-XX:+UseBiasedLocking | 开启偏向锁(JDK6+默认开启) |
-XX:BiasedLockingStartupDelay=0 | 关闭偏向锁启动延迟 |
-XX:-UseBiasedLocking | 关闭偏向锁,直接进入轻量级锁 |