| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> synchronized 原理、使用、锁升级过程,写到我要吐血了 -> 正文阅读 |
|
[Java知识库]synchronized 原理、使用、锁升级过程,写到我要吐血了 |
??多线程编程中,会出现多个线程同时访问 同一个
共享、可变资源的情况,这个资源我们称之其为
临界资源 ;这种资源可以是:
对象 、
变量 、
文件 等。
??由于线程执行的过程是不可控的,所以需要采用同步机制,对对象的可变状态进行访问 。实际上,所有的并发模式在解决线程安全问题时,采用的方案都是 解决方式: ??加锁 !!! ??不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈(工作内存)中,因此不具有共享性,不会导致线程安全问题。 ??Java 中,提供了两种方式来实现同步互斥访问: 1.锁的分类??锁,按照性质的不同,可以分为:
??根据不同标准,Java 锁还可以分为: 2.synchronized 含义??synchronized 是同步锁,用来实现互斥同步。 ??在 Java 中,关键字 synchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作) ??synchronized 还可以保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代 volatile 功能,但是 volatile 更轻量,还是要分场景使用)。
3.synchronized 三种加锁方式
1.修饰实例方法??实例对象锁,就是用 synchronized 修饰
2.修饰静态方法??当 synchronized 作用于静态方法时,其 ??需要注意的是:
3 修饰代码块??在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方法对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。 我们可以使用如下几种对象来作为锁的对象: 1.变量锁??使用 synchronized,锁住的是变量
示例:
2.实例对象锁this 代表当前实例,即 new 出来的当前对象。
3.当前类的 Class 对象锁
4.synchronized 底层原理??synchronized 底层时通过内部对象 ??基于 ??synchronzied 锁,在JDK 6以前,是一个重量级锁,性能较低;JDK6 对 synchronzied 锁进行了比较大的优化,详细优化过程,继续往下看 5.synchronized 锁优化升级过程。 ??synchronized 翻译成汇编指令,就是 ??我们已经了解了 synchronized 的三种加锁方式。每一个 Object 对象在被创建以后,其都会在 JVM 中维护一个与之相对应的 Monitor 对象。该 Monitor 对象就是控制加锁/解锁的对象,我们又叫它 示例: ??t1、t2、t3 三个线程,同时来到 monitorenter 临界点,开始共同竞争该对象中的 Monitor 管程对象。 1.Monitor 管程对象??我们已经知道了 synchronized 加锁 & 解锁,是通过一个叫做 Monitor 的管程对象来控制的。那这个对象在哪里定义的呢?这个对象又是怎么管理这些锁信息的呢?来聊聊 Monitor 管程对象 ?? ??
1.ObjectMonitor 对象属性说明
2.ObjectMonitor 工作流程
3.Java 对象内存结构
??Java 对象,包括:实例对象、类对象。它们两种对象内存结构基本一致。此处以实例对象说明,如图所示: 4._header 对象头介绍(对象如何加锁)??synchronized 锁住的只有这 2 种情况,不是
??HotSpot 虚拟机的对象头包括两部分信息:1.Mark Word ?2.MetaData ?? ??如果对象是数组类型,则需要三个机器码,因为 JVM 虚拟机可以通过 Java 对象的元数据信息确定 Java 对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。(可参考:3.Java 对象内存结构 结构图) ??在 32 位的 HotSpot 虚拟机中,对象在未被锁定的状态下,MarkWord 的 32 个 Bits 空间中的
??在 64 位的 HotSpot 虚拟机中,对象在未被锁定的状态下,MarkWord 的 64 个 Bits 空间中的
源码如图所示: 2.JDK 6 synchronized 锁优化升级过程??在 JDK 6 之前,使用 synchronized 就直接是重量级锁,严重影响性能,被人所诟病。在 JDK 6 中,对 synchronzied 锁进行了一次大的改进。改进后的 synchronized 就属于真香系列了。 ??到这里,相信你对对象头中的 ??锁的状态总共有四种,
1.锁的四种状态介绍1.无锁??初始化时,对象没有被访问,处于无锁状态 2.偏向锁??
??如果JDK6之前,使用 synchronized 修饰,直接就申请一个互斥锁,另一个线程来了,直接阻塞。【会严重影响性能,所以在 synchronized 优化时,才有了锁4种状态】 ??JVM 作者认为大多数线程在进入到锁的状态之后,是没有竞争的,更多的可能是一个单线程的访问,单线程没必要向底层申请一个重量级锁,做一个偏向锁就 OK 了。 3.轻量级锁??顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将 Mark Word 中的部分字节 CAS更新指向线程栈中的 Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。 ??当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。 ??适用场景: ?? ?? 示例场景:
4.重量级锁??重量级锁是依赖对象内部的 monitor 锁来实现的,而 monitor 又依赖操作系统的 Mutex Lock(互斥锁)来实现的,所以重量级锁也称为互斥锁。另一个线程来了,直接阻塞。 为什么重量级线程开销很大的? ??当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗 CPU。 2.synchronized 锁升级流程图3.示例演示 synchronized 锁升级全过程场景: 锁升级场景图:
1.无锁 → 偏向锁??线程 t1 访问,此时锁对象 Mark Word 处于无锁状态,检查锁标志位是否位 01,是的话,检查倒数第3位,是无锁状态还是偏向锁状态。 ??初次进来,还是无锁状态,线程 t1 会将锁升级为偏向锁(利用CAS算法将倒数第三位修改为1)。偏向锁不会自动释放,只有其他线程和他竞争时才会去释放,否则会一直偏向当前线程 t1。 ??此时线程 t1 进入 monitorenter,开始执行同步块内容。 ??线程 t1 此时如果还没执行完,多线程再 CPU 底层调用的是时间片,等到达一个安全点时,此时会再次检查线程 t1 的运行状态。如果线程 t1 运行完毕退出了同步块,此时处于解锁状态,那么通过 CAS 锁,对象会将末三位的状态由 1 变为 0 。然后线程 t2 获取锁,将 Mark Word 对象头中的线程 ID 替换为自己的 ID,开始执行自己的逻辑即可,此时就不需要由偏向锁升级至轻量级锁。 2.偏向锁 → 轻量级锁??到达安全点,并不意味着线程 t1 已经执行完毕。如果线程 t1 还未执行完毕,那么将开始 ??暂停线程 t1 ,通过 CAS 设置锁标志位为 00 (变为轻量级锁)。线程 t1 和 t2 在自己线程栈上开辟一块空间 Lock Record,同时将 Mark Word 对象头数据复制一份到自己的 Lock Record 空间中。并将 Mark Word 中的前 30 位指向线程 t1 栈中所记录的指针。 ??偏向锁 → 轻量级锁,升级成功。 3.无锁 → 轻量级锁??初次进入,属于无锁状态。线程 t1 和 线程 t2 存在竞争。 ??此时两个线程处于并发状态,每个线程都会在当前线程栈上开辟一块空间 Lock Record,同时将 Mark Word 对象头数据复制一份到自己的 Lock Record 空间中。 ??同时还会在 Lock Record 中定义一些变量,比如 owner 等,然后 Mark Word 的前 30 位清空,记录为线程栈中锁记录的指针,同时 Lock Record 的 owner 属性,也有个指针指向 Mark Word, ??此时线程 t1 和线程 t2 就处于一个竞争状态,目前还不知道哪个线程获得锁,owner 属性等都为空。两个线程都开始通过 CAS算法修改 Mark Word 中的指针指向地址,准备升级为轻量级锁。 ??线程 t1 如果修改成功,拿到锁后,Mark Word 中的前 30 位指向线程 t1 栈中所记录的指针。就开始执行自己的同步逻辑块,线程 t2 发现自己修改失败,便进入 4.轻量级锁–> 重量级锁??当线程 t2 自旋一定次数后,发现线程 t1 还没执行完毕,线程 t2 自旋失败后,线程 t2 请求 JVM 进行锁升级,将自己的锁膨胀为重量级锁。同时将 Mark Word 中的前 30 位指针指向重量级锁。 ??线程 t2 会调用 pThread ,去底层申请一个互斥量。此时就会涉及【用户态 → 内存态】的切换,需要调用系统内核申请互斥量,状态转换是一个非常耗时、耗费资源的过程。(pThread参考:提起线程,你不了解的那些事) ??此时,锁的对象头 Mark Word 前 30 位,不再指向当前拥有锁的线程 t1,而是指向重量级锁。然后线程 t2 会调用底层 pThread.Mutex 方法,操作将自己成为阻塞挂起状态。所有的阻塞线程,都会放在 ObjectMonitor 的 waitSet 队列中去。 ??此时线程 t1 执行完同步代码后,开始释放锁,通过 CAS 修改 Mark Word,发现前 30位 指针并不是指向自己线程 t1 。 5.锁粗化 & 锁消除 & 逃逸分析1.锁粗化??原则上,锁的粒度要尽量小,因为这样可以提高并发度,但是假如一系列的连环操作都是对同一个对象反复加锁,解锁,比如把锁加载在循环体里,单次同步操作的时间也许很短,但是高频反复的锁请求、同步和释放,也会对系统资源造成一定消耗,可能还不如加一把大锁。而锁粗化就是增大锁的作用域,把很多次锁的请求合并成一个请求,以此来降低短时间内大量锁请求、同步、释放带来的性能损耗。 ??场景: JVM 会检测到这样一连串的操作都对同一个对象加锁(for 循环内1000次执行append,没有锁粗化得话就要执行1000次加锁/解锁),此时JVM就会将加锁的范围粗化到这一连串的操作的外部(比如for 循环外),使得这一连串操作只需要加一次锁即可。
2.锁消除1.什么是锁消除??虚拟机的即时编译器在运行时,会对一些代码上要求是同步的,但被检测到其实不可能存在共享数据竞争的 ??比如 StringBuffer 的 append 方法用了 synchronized 关键词,它是线程安全的。但我们可能仅在线程内部把 StringBuffer 当作局部变量使用:
??代码中 createStringBuffer 方法中的局部对象 sBuf, ??这时我们可以通过编译器将其优化,将锁消除,前提是 Java 必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析:
??逃逸分析:比如上面的代码,它要看sBuf是否可能逃出它的作用域?,如果将 sBuf 作为方法的返回值进行返回,那么它在方法外部可能被当作一个全局对象使用,就有可能发生线程安全问题,这时就可以说 sBuf 这个对象发生逃逸了,因而不应将 append 操作的锁消除,但我们上面的代码没有发生锁逃逸,锁消除就可以带来一定的性能提升。 2.锁消除实例
??这就说明了逃逸分析把锁消除了,并在性能上得到了很大的提升。这里说明一下Java的逃逸分析是方法级别的,因为JIT的即时编译是方法级别。【除了方法逃逸,还有线程逃逸,继续看下面的逃逸分析】 3.逃逸分析
??实例对象存储在堆区时:实例对象内存存在堆区,实例的引用存在栈上,实例的元数据class存在方法区或者元空间。但是,实例对象并不一定是存在堆区。只有在对象没有线程逃逸行为时,才全部存在堆区。如果发生线程逃逸行为,部分对象是会存在线程栈中的。 1.什么是逃逸分析??逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 逃逸分析(Escape Analysis)算是目前Java虚拟机中比较前沿的优化技术了。Java 从 JDK6 才开始引入该技术。 2.逃逸分析的原理??Java 本身的限制(对象只能分配到堆中),我们可以这么理解,为了减少临时对象在堆内分配的数量,我会在一个方法体内定义一个局部变量,并且该变量在方法执行过程中未发生逃逸,按照 JVM 调优机制,首先会在堆内存创建类的实例,然后将此对象的引用压入调用栈,继续执行,这是 JVM 优化前的方式。 ??然后,我采用逃逸分析对 JVM 进行优化。即针对栈的重新分配方式,首先找出未逃逸的变量,将该变量直接存到栈里,无需进入堆,分配完成后,继续调用栈内执行,最后线程执行结束,栈空间被回收,局部变量也被回收了。如此操作,是优化前在堆中,优化后在栈中,从而减少了堆中对象的分配和销毁,从而优化性能。 3.逃逸的方式??方法逃逸:在一个方法体内,定义一个局部变量,而它可能被外部方法引用,比如作为调用参数传递给方法,或作为对象直接返回。或者,可以理解成对象跳出了方法。 ??线程逃逸:这个对象被其他线程访问到,比如赋值给了实例变量,并被其他线程访问到了。对象逃出了当前线程。 4.逃逸分析,编译器对代码做了如下优化
5.逃逸分析命令
6.代码展示1.创建的对象并没有被方法外使用(发生逃逸)??for 循环创建 5w 个 People 对象,只创建,方法外并没有使用。采用 JDK8 默认开启逃逸分析
??这种情况下创建的 people 实例,并不会被外部其他变量所使用,只是一个局部变量。开启 逃逸分析后,JVM 智能分析,实际 5w 个实例,在堆区只生成了 17792 个,其他实例都存储在当前线程栈中。 2.创建的对象可能会被方法外部使用(未发生逃逸)??for 循环创建 5w 个 People 对象,通过 return 的方式返回,方法外可能会使用。采用 JDK8 默认开启逃逸分析
??这种情况下创建的 people 实例,通过 return 方式返回。JVM 分析到这些实例 ??2021-11-23,《synchronized 原理、使用、锁升级过程,写到我要吐血了》已更新,接下来将讲解: Lock 锁,ReentrantLock 可重入锁,ReentrantLock 源码解析,AQS 同步队列,Condition条件队列,如有需要,请持续关注《并发编程》板块!!!
博主写作不易,加个关注呗 求关注、求点赞,加个关注不迷路 ヾ(?°?°?)ノ゙ 我不能保证所写的内容都正确,但是可以保证不复制、不粘贴。保证每一句话、每一行代码都是亲手敲过的,错误也请指出,望轻喷 Thanks?(・ω・)ノ |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 3:46:01- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |