synchronized
线程锁
1.1synchronized的认识
1.1.1synchronized的介绍
在多线程并发编程中,synchronized关键字是重量级锁的代名词。但是,随着JDK的发展,对synchronized底层进行了各种优化后,有些情况它就并不那么重了,JDK 1.6中为了减少获取锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁的概念,以及锁
的存储结构和升级的过程。
1.1.2synchronized的使用
synchronized有三种加锁方式:
- 修饰实例方法,作用于当前实例对象,进入同步代码前需要获得当前实例的锁
- 修饰静态方法,作用于当前类对象,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块之前要获得给定对象的锁
class SynchronizedDemo {
// 类锁(修饰静态方法:锁当前类的 Class 对象。)
public static synchronized void inStaticMethod() {
for (int i = 0; i < 10; i++) {
System.out.println("aaa");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 类锁(修饰代码块,锁括号中的 Class 对象)
public static void inStaticMethodLockClassObj() {
synchronized(SynchronizedDemo.class){
for (int i = 0; i < 10; i++) {
System.out.println("aaa");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 对象锁(修饰普通方法:锁当前实例对象)
public synchronized void inNormalMethod() {
for (int i = 0; i < 10; i++) {
System.out.println("bbb");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 对象锁(修饰代码块:锁括号中的实例对象)
public void bb() {
synchronized(this){
for (int i = 0; i < 10; i++) {
System.out.println("bbb");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 无锁
public void cc() {
for (int i = 0; i < 10; i++) {
System.out.println("ccc");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.1.3synchronized关键字的原理
synchronized关键字的实现是依赖JVM的,需要了解几个概念。
1.对象头
在JVM的堆内存中,每个对象主要由三部分构成:对象头、实例变量、填充字节。
一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。
- 对象头包括标记字段、类型指针
标记字段(Mark Word):用于存储对象自身的运行数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向锁ID等等。
一般占用两个机器码。在32-bit JVM上占用64bit,在64-bit JVM中占用128bit即16bytes。
32位虚拟机上Mark Word的存储:
64位虚拟机上Mark Word的存储:
- 实例变量:存储对象的属性信息,包括父类的属性信息,按照4字节对齐
- 填充字节:因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍
2.Monitor对象 - Monitor是一种用来实现同步的工具
- 与每个JAVA对象关联,所有JAVA对象天生携带Monitor
- Monitor是实现Sychronized的基础
对象监视器(Monitor)由ObjectMonitor对象实现(c++),其跟同步相关的数据结构如下:
ObjectMonitor() {
_count = 0; //用来记录该对象被线程获取锁的次数
_waiters = 0;
_recursions = 0; //锁的重入次数
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
}
1.1.3synchronized锁升级
JDK1.6对synchronized加锁进行了优化,其优化的核心就是锁升级:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
无锁状态:对象头中Mark Word的偏向锁标志位为0,默认后两个为01,标志该对象可进行偏向;
无锁 -> 偏向锁
当线程第一次进入synchronized关键字锁住代码块的时候,也就是锁对象第一次被线程获取时,锁对象进入偏向状态,同时使用CAS操作将线程ID记录到Mark Word中,偏向锁不会主动释放,所以当这个线程在此请求锁时,无需在重新获取锁。
所以偏向锁在对于没有锁竞争的情况下,偏向锁有很好的优化;
偏向锁 -> 轻量级锁
当有其他线程尝试竞争偏向锁时,则还是通过CAS操作竞争锁,这里并不会直接升级锁,原因是偏向锁不会主动释放,因此即使之前的线程执行完毕之后,该锁对象存储的还是上一个线程ID。具体操作是
会检测当前锁存储的线程ID是否还存活,如果没有存活,则将锁标志位置为无锁状态,线程即可通过CAS重复之前偏向锁的步骤;如果之前的线程仍然存活,则检查该线程的栈帧信息,如果需要继续持有这个锁对象,那么暂停该线程,撤销偏向锁,升级为轻量级锁,若不在使用该锁对象,继续上一步,置为无锁,进行CAS竞争偏向。
轻量级锁 -> 重量级锁
每个线程的栈帧都会包含一个锁记录 Lock Record的结构,内部可以存储锁对象的Mark Word,让锁记录Object reference指向锁对象,并尝试用CAS替换锁对象的对象头中的Mark Word替换为执行锁记录的指针,成功则当前线程获取锁,失败这又分为两种状态:1.表示其他线程获取当前锁对象,用自旋的方式获取锁,自旋获取锁失败则会膨胀为重量级锁。2.当前线程自己执行了锁重入,那么会在添加一条Lock Record重入锁的记录。
轻量级锁的目的是在多线程交替执行同步代码块时(未发生竞争),避免使用互斥锁(重量级锁)带来的性能消耗。这一种选择的想法是,短时间的自旋,换取线程在用户态和内核态之间切换的开销。
重量级锁
重量级锁是通过锁对象内部的监视器(monitor)实现的。其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间缓慢。
插一张网上很好的原理图:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?