【多线程: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就认为竞争激烈,就把这个类下的全部对象的锁升级为轻量级锁或重量级锁

目录
相关文章
|
17天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
|
2月前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
140 29
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
1月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
47 4
|
1月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
25 2
|
1月前
|
Java 编译器 程序员
【多线程】synchronized原理
【多线程】synchronized原理
59 0
|
1月前
|
Java 应用服务中间件 API
nginx线程池原理
nginx线程池原理
36 0
|
2月前
|
Java Android开发 开发者
安卓应用开发中的线程管理优化技巧
【9月更文挑战第10天】在安卓开发的海洋里,线程管理犹如航行的风帆,掌握好它,能让应用乘风破浪,反之则可能遭遇性能的暗礁。本文将通过浅显易懂的语言和生动的比喻,带你探索如何优雅地处理安卓中的线程问题,从基础的线程创建到高级的线程池运用,让你的应用运行更加流畅。
|
2月前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。
|
2月前
|
存储 缓存 Java
JAVA并发编程系列(11)线程池底层原理架构剖析
本文详细解析了Java线程池的核心参数及其意义,包括核心线程数量(corePoolSize)、最大线程数量(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务存储队列(workQueue)、线程工厂(threadFactory)及拒绝策略(handler)。此外,还介绍了四种常见的线程池:可缓存线程池(newCachedThreadPool)、定时调度线程池(newScheduledThreadPool)、单线程池(newSingleThreadExecutor)及固定长度线程池(newFixedThreadPool)。
|
2月前
|
SQL 存储 监控
SQLServer事务复制延迟优化之并行(多线程)复制
【9月更文挑战第12天】在SQL Server中,事务复制延迟会影响数据同步性。并行复制可通过多线程处理优化这一问题,提高复制效率。主要优化方法包括:配置分发代理参数、优化网络带宽、调整系统资源、优化数据库设计及定期监控维护。合理实施这些措施可提升数据同步的及时性和可靠性。