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

目录
相关文章
|
5天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
15 0
|
21天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
21天前
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
【4月更文挑战第6天】Java中的`synchronized`关键字用于处理多线程并发,确保共享资源的线程安全。它可以修饰方法或代码块,实现互斥访问。当用于方法时,锁定对象实例或类对象;用于代码块时,锁定指定对象。过度使用可能导致性能问题,应注意避免锁持有时间过长、死锁,并考虑使用`java.util.concurrent`包中的高级工具。正确理解和使用`synchronized`是编写线程安全程序的关键。
|
24天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第3天】 在Java并发编程中,线程池是一种重要的资源管理工具,它能有效地控制和管理线程的数量,提高系统性能。本文将深入探讨Java线程池的工作原理、应用场景以及优化策略,帮助读者更好地理解和应用线程池。
|
2天前
|
消息中间件 缓存 NoSQL
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
|
3天前
|
安全 Java 编译器
是时候来唠一唠synchronized关键字了,Java多线程的必问考点!
本文简要介绍了Java中的`synchronized`关键字,它是用于保证多线程环境下的同步,解决原子性、可见性和顺序性问题。从JDK1.6开始,synchronized进行了优化,性能得到提升,现在仍可在项目中使用。synchronized有三种用法:修饰实例方法、静态方法和代码块。文章还讨论了synchronized修饰代码块的锁对象、静态与非静态方法调用的互斥性,以及构造方法不能被同步修饰。此外,通过反汇编展示了`synchronized`在方法和代码块上的底层实现,涉及ObjectMonitor和monitorenter/monitorexit指令。
17 0
|
10天前
|
Java 开发者
Java中多线程并发控制的实现与优化
【4月更文挑战第17天】 在现代软件开发中,多线程编程已成为提升应用性能和响应能力的关键手段。特别是在Java语言中,由于其平台无关性和强大的运行时环境,多线程技术的应用尤为广泛。本文将深入探讨Java多线程的并发控制机制,包括基本的同步方法、死锁问题以及高级并发工具如java.util.concurrent包的使用。通过分析多线程环境下的竞态条件、资源争夺和线程协调问题,我们提出了一系列实现和优化策略,旨在帮助开发者构建更加健壮、高效的多线程应用。
7 0
|
13天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
25天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第2天】本文将深入探讨Java并发编程中的重要组件——线程池。我们将了解线程池的基本概念,应用场景,以及如何优化线程池的性能。通过本文,你将能够更好地理解和使用线程池,提高你的Java并发编程能力。
|
27天前
|
存储 安全 Java
Java线程池ThreadPoolExcutor源码解读详解04-阻塞队列之PriorityBlockingQueue原理及扩容机制详解
1. **继承实现图关系**: - `PriorityBlockingQueue`实现了`BlockingQueue`接口,提供了线程安全的队列操作。 - 内部基于优先级堆(小顶堆或大顶堆)的数据结构实现,可以保证元素按照优先级顺序出队。 2. **底层数据存储结构**: - 默认容量是11,存储数据的数组会在需要时动态扩容。 - 数组长度总是2的幂,以满足堆的性质。 3. **构造器**: - 无参构造器创建一个默认容量的队列,元素需要实现`Comparable`接口。 - 指定容量构造器允许设置初始容量,但不指定排序规则。 - 可指定容量和比较
42 2