首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

Java 并发 —— Synchronized 关键字和锁升级 【可下载 Synchronized_思维导图】

  • 25-03-07 20:01
  • 4779
  • 12644
blog.csdn.net

目录

一、Synchronized使用场景

1.1 为什么需要上锁?

1.2 Synchronized 哪些方式同步

二、Synchronized实现原理

2.1 markword 内存布局

2.2 Synchronized 在 jvm 实现原理

三、Synchronized 锁升级

3.1 锁升级为什么这样设计?

3.1.1 偏向锁

3.1.2 轻量级锁(自旋锁)

四、Synchronized 思维导图(可下载)


一、Synchronized使用场景

Synchronized 是一个 Java 同步关键字,在某些多线程场景下,如果不进行同步会导致数据不安全,而 Synchronized 关键字就是用于代码同步。什么情况下会数据不安全呢,要满足两个条件:一是数据共享(临界资源),二是多线程同时访问并改变该数据。

下面是模拟火车票售卖的示例程序,具体如下:

  1. // Java 并发 —— Synchronized 关键字和锁升级 【争取小白也能看懂】
  2. // 线程操作资源类 - 共享资源
  3. class TrainTicketSale {
  4. // 火车票剩余张数
  5. int remainTicketNum = 1_000;
  6. // 卖火车票,卖一张少一张,返回true 表示售票成功,返回false 表示售票失败
  7. public synchronized boolean sale() {
  8. if (remainTicketNum > 0 ) {
  9. // 模拟网络延时
  10. try {
  11. TimeUnit.MILLISECONDS.sleep(1);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. System.out.println(System.currentTimeMillis() + "\t" + Thread.currentThread().getName() + "\t剩:" + --remainTicketNum);
  16. return true;
  17. }
  18. return false;
  19. }
  20. // 获取还剩下多少张火车票
  21. public int getRemainTicketNum() {
  22. return remainTicketNum;
  23. }
  24. }
  25. public class T05_Juc_SynchronizedAtomic {
  26. public static void main(String[] args) throws Exception {
  27. TrainTicketSale sale = new TrainTicketSale();
  28. // 运行结果:火车票还剩下:-1 张
  29. System.out.println("火车票还剩下:" + sale.getRemainTicketNum() + " 张");
  30. Vector vector = new Vector<>(10);
  31. for (int i = 0; i < 10; i++) {
  32. Thread thread = new Thread(() -> {
  33. while (true) {
  34. if (!sale.sale()) break;
  35. }
  36. }, "线程名:" + i);
  37. vector.add(thread);
  38. thread.start();
  39. }
  40. // 等待售票线程卖票完毕
  41. for (Thread thread : vector) {
  42. thread.join();
  43. }
  44. // 运行结果:如果没有加 synchronized 关键字,票会出现超卖现象;添加了synchronized 关键字测不会出现超卖
  45. System.out.println(" Finally 火车票还剩下:" + sale.getRemainTicketNum() + " 张");
  46. }
  47. }

测试一:没有使用 Synchronized 关键字同步售卖 sale() 方法,火车票出现了超卖现象。 

 测试二:使用 Synchronized 关键字同步售卖 sale() 方法,火车票没有出现超卖问题了,解决共享资源在多线程下数据同步问题


1.1 为什么需要上锁?

多个线程去访问同一个资源的时候,访问某一段代码或者某临界资源的时候是需要有一把锁的概念的。

比如:我们对一个数字做递增,两个程序对它一块儿来做递增,递增就是把一个程序往上加1,如果两个线程共同访问的时候,第一个线程读它是0,然后把它加1,在自己线程内部内存里面算还没有写回去的时候,第二个线程读到了它还是0,加1再写回去,本来加了两次,但是还是1。

那么我们在对这个数字递增的过程中上把锁,就是说第一个线程对这个数字访问的时候是独占的,不允许别的线程来访问,不允许别的线程来对它进行计算,我必须加完1再释放锁,其他线程才能对它继续加。实质上,这把锁并不是对数字进行锁定的,你可以任意指定,想锁谁就锁谁。


1.2 Synchronized 哪些方式同步

我第一个程序是这么写的,如果说你想上了把锁之后才能对count进行减减访问,你可以new一个Object,所以这里锁定就是lock,当我拿到这把锁的时候才能执行这段代码。

1. Synchronized 修饰同步代码块:锁对象是 Synchronized 后面括号里配置的对象,这个对象可以是某个对象(xLock)

2.Synchronized 修饰同步代码块:锁对象是 Synchronized 后面括号里配置的对象,这个对象可以是某个对象(xLock)

3.Synchronized 修饰普通同步方法:锁对象当前实例对象,实际上是对调用该方法的对象加锁,俗称“对象锁”

4.Synchronized 修饰静态同步方法:锁对象是当前的类 Class 对象,实际上是对该类 Class 对象加锁,俗称“类锁”。

总结一下:Synchronized 锁的3种使用形式(使用场景):

  1. Synchronized 修饰普通同步方法:锁对象当前实例对象;
  2. Synchronized 修饰静态同步方法:锁对象是当前的类 Class 对象;
  3. Synchronized 修饰同步代码块:锁对象是 Synchronized 后面括号里配置的对象,这个对象可以是某个对象(xLock),也可以是某个类(TLock.class);

二、Synchronized实现原理

2.1 markword 内存布局

Java对象由三部分构成:对象头、实例数据、对齐补充。

  • 对象头:存储对象自身运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志等;
  • class pointer:类型指针,存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐;
  • 对齐补充:8字节对齐补充;

通过第一部分可以知道,Synchronized 不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,那么 Synchronized锁对象是存在哪里的呢?答案是存在锁对象的对象头的 MarkWord中。那么 MarkWord在对象头中到底长什么样,也就是它到底存储了什么呢?

Synchronized 优化的过程和 markword 息息相关,用 markword 中最低的三位代表锁状态  其中1位是偏向锁位  两位是普通锁位

上图中的偏向锁和轻量级锁都是在 jdk 6 以后对锁机制进行优化时引进的,下文的锁升级部分会具体讲解,Synchronized 关键字对应的是重量级锁,接下来对重量级锁在 Hotspot JVM中的实现锁讲解。


2.2 Synchronized 在 jvm 实现原理

在之前的博客文章中也有介绍 Synchronized 相关知识:Java 内存模型 —— 用示例剖析 ,下面我们做一个 Synchronized 同步代码块的代码及对应的字节码,如下:

可以看出同步方法块在进入代码块时插入了 monitorenter 语句,在退出代码块时插入了 monitorexit 语句,为了保证不论是正常执行完毕(第6行)还是异常跳出代码块(第12行)都能执行monitorexit语句,因此会出现两句monitorexit语句。

同时,我们得出这样一个结论:Synchronized自动上锁,自动释放锁

出于兴趣,继续深入 monitorenter 细节,发现 monitorenter 是由C++ 实现的,在 InterpreterRuntime.cpp文件中第608行moniterenter 方法,moniterenter 方法说明,如果使用了偏向锁则执行 fast_enter;如果没有使用偏向锁则执行slow_enter。


三、Synchronized 锁升级

在 JDK早期,Synchronized 叫做重量级锁,jvm 向操作系统申请一把锁,都需要从用户态向系统态的一个转换 Synchronized(lock)  在jdk1.2之前,由OS帮忙管理线程,整个实现过程复杂涉及用户空间向系统空间申请锁,属于重量级锁。

在 JDK后期(jdk 1.6),Synchronized 有锁升级过程,无锁 -> 偏向锁 -> 自旋锁 -> 重量级锁(操作系统OS管理)。这样一来,避免一上来就跟系统空间申请锁资源,比如:单线程场景 或 多线程下竞争不激烈时,先由 jvm 来实现共享资源数据的同步,属于轻量级锁。

话不多说,直接 Synchronized 锁升级流程图,如下:

如上图所示,图中有偏向锁。默认情况 偏向锁启动有个时延,默认是4秒

why? 因为 JVM 虚拟机自己有一些默认启动的线程,里面有多好 sync 代码,这些 sync 代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断地进行锁撤销和锁升级的操作,效率较低,可以通过参数:-XX:BiasedLockingStartDelay=0 调整偏向锁启动时间,不过一般我们不会调节它的。

Synchronized 锁升级流程:

  1. 如果有线程上锁,上偏向锁,指的就是,把 markword 的线程ID改为自己线程ID的过程,偏向锁不可重偏向;
  2. 如果有线程竞争,撤销偏向锁,升级轻量级锁,线程在自己的线程栈生成 LockRecord,用 CAS 操作将 markword 设置为指向自己这个线程的 LR 的指针,设置成功者得到锁。
  3. 如果竞争加剧,竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半,1.6之后,加入自适应自旋 adapative Self Spinning,jvm 自己控制

总结:

  • new - 偏向锁 - 轻量级锁(也叫无锁,自旋锁,自适应自旋)- 重量级锁
  • Synchronized 优化的过程和 markword 息息相关
  • 用 markword 中最低的三位代表锁状态  其中1位是偏向锁位  两位是普通锁位

3.1 锁升级为什么这样设计?

锁的4种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)

3.1.1 偏向锁

为什么要引入偏向锁?

  • 因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

偏向锁的升级

当线程1 访问代码块并获取锁对象时,会在 java对象头和栈帧中记录偏向的锁的 threadID,因为偏向锁不会主动释放锁,因此以后线程1 再次获取锁的时候,需要比较当前线程的 threadID 和 Java 对象头中的 threadID 是否一致,如果一致(还是线程1获取锁对象),则无需使用 CAS 来加锁、解锁;如果不一致(其他线程,如线程2 要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1 的 threadID),那么需要查看 Java 对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

偏向锁撤销

为什么频繁的偏向锁撤销会导致STW时间增加呢?阅读偏向锁源码可以知道:偏向锁的撤销需要等待全局安全点(safe point),暂停持有偏向锁的线程,检查持有偏向锁的线程状态。首先遍历当前JVM的所有线程,如果能找到偏向线程,则说明偏向的线程还存活,此时检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁。可以看出撤销偏向锁的时候会导致stop the word。

偏向锁的优化(取消或延迟):

因为偏向锁的撤销是非常重的操作,甚至会导致STW,那么我们可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁。偏向锁默认是系统启动几秒钟后启用偏向锁,默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能,可以使用参数进行设置:-XX:BiasedLockingStartupDelay=5


3.1.2 轻量级锁(自旋锁)

为什么要引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋等待锁释放。

偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁;JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开。

为什么有自旋锁,还需要重要级锁?

自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗;重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源,重量级锁会通过操作系统,将线程放入等待队列_WaitSet

轻量级锁什么时候升级为重量级锁?

线程1 获取轻量级锁时会先把锁对象的对象头 MarkWord 复制一份到线程1 的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用 CAS 把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;

如果在线程1 复制对象头的同时(在线程1 CAS 之前),线程2 也准备获取锁,复制了对象头到线程2 的锁记录空间中,但是在线程2 CAS 的时候,发现线程1 已经把对象头换了,线程2 的 CAS 失败,那么线程2 就尝试使用自旋锁来等待线程1 释放锁。

但是如果自旋的时间太长也不行,因为自旋是要消耗 CPU 的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1 还没有释放锁,或者线程1 还在执行,线程2 还在自旋等待,这时又有一个线程3 过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

 

*注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。

这几种锁的优缺点(偏向锁、轻量级锁、重量级锁)

四、Synchronized 思维导图(可下载)

Synchronized 思维导图下载链接:Synchronized_思维导图(全面).xmind.zip

大纲如下:

 


文章最后,给大家推荐一些受欢迎的技术博客链接:

  1. JAVA相关的深度技术博客链接
  2. Flink 相关技术博客链接
  3. Spark 核心技术链接
  4. 设计模式 —— 深度技术博客链接
  5. 机器学习 —— 深度技术博客链接
  6. Hadoop相关技术博客链接
  7. 超全干货--Flink思维导图,花了3周左右编写、校对
  8. 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
  9. 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
  10. 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
  11. 深入聊聊Java 垃圾回收机制【附原理图及调优方法】

欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!

                                           

       

 

注:本文转载自blog.csdn.net的不埋雷的探长的文章"https://blog.csdn.net/weixin_32265569/article/details/108168792#%E5%9B%9B%E3%80%81Synchronized%20%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE%EF%BC%88%E5%8F%AF%E4%B8%8B%E8%BD%BD%EF%BC%89"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top