【多线程:synchronized优化原理】

简介: 【多线程:synchronized优化原理】

【多线程:synchronized优化原理】

01.介绍

synchronized优化主要是在四个方面:重量级锁,轻量级锁,自旋锁,偏向锁,接下来的内容都会讲解这些锁。

Mark Word结构

Mark Word(32bits) 加锁状态 State
hashcode:25 age:4 biased_lock:0 01 Normal
thread:23 epoch:2 age:4 biased_lock:1 01 Biased
ptr_to_lock_record:30 00 Lightweight Locked
ptr_to_heavyweight_montior:30 10 Heavyweight Locked
11 Marked for GC

小故事(黑马juc的例子)

故事角色
老王 - JVM/操作系统
小南 - 线程
小女 - 线程
房间 - 对象
房间门上 - 防盗锁 - Monitor
房间门上 - 小南书包 - 轻量级锁
房间门上 - 刻上小南大名 - 偏向锁
批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,
即使他离开了,别人也进不了门,他的工作就是安全的。

但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女
晚上用。每次上锁太麻烦了,有没有更简单的办法呢?

小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因
此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是
自己的,那么就在门外等,并通知对方下次用锁门的方式。

后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍
然觉得麻烦。

于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那
么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦
掉,升级为挂书包的方式。

同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老
家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老
王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包

这个故事就是类比与上述几种锁,各位可以在理解不清楚时 回来类比看看。

把握住一个点:轻量级锁

轻量级锁如果开始竞争 则变为重量级锁

轻量级锁如果长时间是自己一个线程 则变为偏向锁

轻量级锁 在变为重量级锁的过程中 竞争的线程并不会直接进入Monitor的阻塞列表 而是会先变为自旋锁 看是否有机会直接获取锁

02.重量级锁

重量级锁就是我们上个文章讲的Monitor,详情可以看上个文章。

03.轻量级锁

应用场景

如果一个对象虽然有多个线程访问,但是多线程访问的时间是错开的(也就是没有竞争),那么可以用轻量级锁来优化,有同学可以会有疑惑既然都没有竞争了为什么要用多线程,因为只是大部分时间是没有竞争 还是会出现有竞争的情况 此时就要把锁变为重量级锁

注意

轻量级锁对使用者是透明的,也就是语法上还是synchronized

例子

假设有两个方法同步块,利用一个对象加锁

Thread-0线程

static final Object obj = new Object();
public static void method1(){
    synchronized(obj){
        // 同步块A
        method2();
    }
}
public static void method2(){
    synchronized(obj){
        // 同步块B
    }
}

解释

上述代码的目的是用来模拟轻量级锁,并用下面的图片解释

202207140118282.png

本图含义为,我们又一个线程Thread0 有一个监视器Object,可以看出线程的地址初始化是00 代表着轻量级锁也就是MarkWord里的加锁状态,然后 对象的对象头中MarkWord是Hsahcode Age Bias 01也就是normal状态

202207140118860.png

接下来 我们可以看到 通过Lock Record中的Object reference找到对应的对象,之后让Lock Record中的 ==lock record 地址 00==与Object里的==Hashcode Age Bias 01==进行交换,注意这步交换叫做cas

202207140118658.png

cas完毕后Object中==lock record 地址 00==记录了Thread0的地址 与 现在Object对象的加锁状态为00 也就是轻量级锁

202207140118087.png

如果cas操作失败的话,会有两种情况,第一种是其他线程持有了Object对象的轻量级锁 这是说明发生了竞争 进入==锁膨胀==过程,第二种情况是自己执行了synchronized锁重入,那么再加一条Lock Record仅仅作为计数 里面的内容为null 此时的这种情况叫做==锁重入==。

很明显这里的情况是第二种,锁重入。

202207140118087.png

当退出synchronized代码块时(解锁时) 如果取值为null 说明有重入,这时将重置锁记录,表示重入计数减一

202207140118658.png

当退出synchronized代码块时(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头

如果成功 说明解锁成功

如果失败 说明轻量级锁已经进行了锁膨胀或者已经升级为了重量级锁,进入重量级锁的解锁过程

04.锁膨胀(轻量级锁变为重量级锁)

如果在尝试加轻量级锁的过程中cas操作无法成功,这时一种情况就是有其他线程对此对象已经加上的轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

Thread-1线程

static final Object obj = new Object();
public static void method1(){
    synchronized(obj){
        // 同步块A
    }
}

202207140150193.png

当Thread1进行轻量级加锁时,Thread0已经对该对象加了轻量级锁,所以此时Thread1加轻量级锁失败,进入了锁膨胀流程

202207140201759.png

此时Thread-1加锁失败申请变为重量级锁,进入锁膨胀过程,即Object对象申请Monitor锁 让Object指向重量级锁地址,然后自己进入Monitor的EntryList 的阻塞列表 也就是BLOCKED状态

当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头失败,因为此时的Object对象中的Mark Word中记录的是Monitor地址,所以这时会进入重量级解锁流程,即按照Monitor地址 找到 Monitor对象 把Object对象的原数据由Thread-0线程存入Monitor里,设置Owner为null,唤醒EntryList中BLOCKED线程

注意:这里的锁膨胀过程 Thread-1进入阻塞列表前其实还有一段自旋过程,也就是后续的自旋锁

05.自旋锁

当我们的轻量级锁在锁膨胀变为重量级锁的过程中,Thread1线程不会直接就进入阻塞列表,它会进入一个循环 然后一直试探Thread0线程是否已经解锁,这样做的目的是 避免进入阻塞状态 减少不必要的上下文切换 增加执行速度。

注意

需要注意的是 自旋锁是指锁膨胀过程中,如果自旋锁过程中成功获取到了cpu 则依然会处于轻量级锁状态,如果自旋过程失败 则会升级为重量级锁

06.偏向锁(对轻量级锁的优化)

偏向锁是对轻量级锁的优化,既然是优化就说明轻量级锁还有缺点,我们来看下面这个例子

static final Object obj = new Object();
public static void method1(){
    synchronized(obj){
        // 同步块A
        method2();
    }
}
public static void method2(){
    synchronized(obj){
        // 同步块B
        method3();
    }
}
public static void method3(){
    synchronized(obj){
        // 同步块B
    }
}

这个代码我们如果用轻量级锁就会导致锁重入,也就是除了第一次的锁记录替换了对象的MarkWord,剩下的几次锁记录都是null 但是 依然会进行与MarkWord的替换操作 虽然替换失败 但是 替换本身就是一个cas,耗费时间,这就是我们的优化点。

偏向锁如何优化

知道上述轻量级锁的缺点,偏向锁就对症下药,为了避免锁重入的cas操作 所以偏向锁的做法是:第一次调用线程时 用ThreadId替换MarkWord,之后如果再次调用这个线程 只需要检查这个ThreadId是否是自己的,这样就不用进行替换操作避免了不必要的cas

用图片进行对比

轻量级锁

202207140245361.png

偏向锁

202207140245396.png

偏向锁状态详解

如何判断是否为偏向锁,是通过MarkWord里的==biased_lock==,如果为0说明不是偏向锁,如果是1说明是偏向锁。

偏向锁的几种情况

1.偏向锁默认延迟一段时间生效,也就是一个线程最开始是normal状态 之后变为 Biased状态

2.偏向锁也可以通过jvm设置为立即生效

3.当竞争非常激烈是不建议使用偏向锁,可以通过jvm设置关闭偏向锁

4.当监视器对象 调用hashCode方法后 便禁用了偏向锁,原因需要从MarkWord的结果进行分析 因为normal状态与Biased状态的区别在于normal状态有hashCode站25位 但是hashCode是在调用hashCode方法之后才会出现的,hashCode一旦出现 则占用25位,这样Biased状态的==thread:23 epoch:2==就被占用 所以便不能再使用偏向锁

偏向锁:撤销

情况一

当有另外一个与之竞争相互错开的线程也想用同一个对象的偏向锁时,则会进行锁升级 升级为轻量级锁,注意前提是竞争相互错开 如果产生竞争 则会升级为重量级锁

情况二

当使用wait/notify时也会撤销偏向锁 升级为重量级锁,原因是wait/notify只有在重量级锁时才会使用

批量重偏向

批量重偏向的目的是为了对偏向锁进行优化,优化点在于:虽然有多个线程 但是这多个线程的时间是错开的 那么就会变成轻量级锁 但是会出现这种情况 比如Thread0 创建了30个锁每个锁都执行一遍此时偏向Thread0是 然后 Thread1错开执行了这30个锁 对于每个锁都是由原来的偏向Thread0的锁变为轻量级锁,很明显万一这个锁不止30个 更多呢,那么每一个锁都会经历一次偏向锁变为轻量级锁的过程,所以这个就是优化点。

我们默认进行20次换锁之后 就把偏向锁偏向当前线程

批量撤销

当我们撤销次数达到40次之后(可能是先撤销20次 变为偏向了另一个线程,然后又被其他线程撤销了20次 此时便不会偏向当前线程,而是直接撤销全部),jvm就认为竞争激烈,就把这个类下的全部对象的锁升级为轻量级锁或重量级锁

目录
相关文章
|
6天前
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
25 6
|
27天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
26天前
|
并行计算 算法 安全
面试必问的多线程优化技巧与实战
多线程编程是现代软件开发中不可或缺的一部分,特别是在处理高并发场景和优化程序性能时。作为Java开发者,掌握多线程优化技巧不仅能够提升程序的执行效率,还能在面试中脱颖而出。本文将从多线程基础、线程与进程的区别、多线程的优势出发,深入探讨如何避免死锁与竞态条件、线程间的通信机制、线程池的使用优势、线程优化算法与数据结构的选择,以及硬件加速技术。通过多个Java示例,我们将揭示这些技术的底层原理与实现方法。
80 3
|
28天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
54 3
|
1月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
2月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
52 4
|
3月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
82 4
|
3月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
37 2
|
3月前
|
Java 编译器 程序员
【多线程】synchronized原理
【多线程】synchronized原理
72 0
|
3月前
|
Java 应用服务中间件 API
nginx线程池原理
nginx线程池原理
48 0

相关实验场景

更多