当前位置: 首页 > news >正文

一篇认识synchronized锁

要讲明白 synchronized 首先得从计算机的发展史说起。

CPU 的速度非常快,但是内存的速度比较慢。为了缓解 CPU 和内存速度不一致的问题就有了三级缓存。一级和二级缓存是 CPU 核心私有的,但是第三级缓存是共享的。

可见性问题

在多线程并发的环境下,当某一个 CPU 核心去执行某个线程任务时,把内存中的数据读到缓存中,然后修改了缓存中的数据。当缓存数据还没有同步到主存时,线程 B 从主存中读数据,读的不就是旧数据了吗?一个线程修改了共享数据,但是另外一个线程无法立刻获取到最新的共享数据,

有序性问题

执行代码的时候 CPU 或者编译器可能会对指令进行重排序,导致执行顺序和代码书写顺序不一致。还有一个叫原子性问题,这个就不用多介绍了。

synchronize 如何解决这些问题?

能保证某一段代码同一时间只能被拿到锁的线程访问,这就是保证了原子性

synchronized 的编译之后就是 monitorenter 和 monitor exit 两个指令。monitor enter 就是加锁,加锁的时候会使用读屏障强行去从主存重新读取数据,也就是说它能保证读到的数据都是最新的。
monitor exit 就是解锁,解锁的时候通过写屏障保证强制将 CPU 缓存中的变量刷新到主存中,能够保证线程修改后的数据对其他线程立刻可见,这就是保证了可见性

synchronized 能够通过内存屏障防止指令重排,这就是有序性

synchronized 怎么用?

它修饰普通方法锁的就是 当前对象,也就是说一个对象用一把锁,修饰静态方法,锁的是类.class 对象,也就是类的所有对象共用一把锁,修饰代码块,那你括号里写什么,它就锁什么。

锁升级?那为什么要做锁升级?

java 线程的模型是一对一的,每一个 java 线程都直接对应一个操作系统的内核级线程,每次切换线程都需要操作系统从用户态切换到内核态,开销很大,这也就是之前 synchronized 的问题所在。那你既然线程阻塞唤醒它比较慢,那在低并发的情况下,锁竞争比较少的情况下,只要你不阻塞操作系统,就不需要从用户态切换到内核态,就减少了开销,就提高了性能,并发量比较高,锁竞争比较激烈的时候,我再让你去阻塞,这不就完了?这就有了锁升级的过程,从无锁到偏向锁到轻量级锁,到重量级锁。

最开始系统是无锁状态,当有第一个线程来访问同步代码块时,JVM 将对象头的 Mark word 锁标志位设置为偏向锁,然后将线程 id 记录到 Mark word 里面,这个时候线程进入同步代码块就不需要其他的同步操作了,非常的轻,非常的快。
为什么要有偏向锁?偏向锁考虑的是那种只有一个线程抢锁的场景。什么时候升级到轻量级锁?当第二个线程来抢锁就升级为轻量级锁,第二个线程拿不到锁就采用 cas 加自旋的方式不断重新尝试获取锁。
为什么要有轻量级锁?轻量级锁考虑的是竞争锁的线程不多,而且线程持有锁的时间也不长的一个情景。因为阻塞线程需要操作系统从用户态切换到内核态,代价比较大。如果刚刚才阻塞了线程,锁就被释放了,这个代价就有点得不偿失了。因此这个时候就干脆不阻塞线程了,让它自旋等待锁释放。自旋的性能一定要比阻塞的性能好吗?自旋是让线程一直循环执行抢锁命令,线程就是一直在运行的,它没有被阻塞,所以就减少了线程上下文切换的开销,操作系统就不要切换到内核态了。短时间的自旋性能是不错的,但是长时间的自旋会让 CPU 一直在空转,CPU 没有办法执行其他任务,会浪费 CPU。所以这里轻量级锁只放在了两个线程竞争的场景下使用,而且这里的自旋还是适应性自旋,自旋的时间由上一个线程自旋的时间去决定。可以看到偏向锁只记录线程 id,而轻量级锁只是自旋抢锁而已,它们都没有做操作系统级的线程阻塞和切换,不需要操作系统切换到内核态去做操作,整个过程只在用户态就可以完成了。所以在较低的锁竞争的时候,偏向锁和轻量级锁的设计就提高了性能。

轻量级锁什么时候升级为重量级锁?当第二个线程自旋到一定次数之后还是没拿到锁,获取锁失败了,或者当有更多的线程来抢锁了,那就升级为重量级锁。重量级锁加锁就需要调用操作系统的底层原语 mutex,所以每次切换线程都需要操作系统切换到内核态,开销很大。

这也就是为什么称它为重量级锁,为啥非得要升级到重量级锁?我一直轻量级锁不行吗?因为自旋它只适合于锁竞争比较小,而且执行时间比较短的一个程序。但有很多线程来抢锁,所以要大量的线程去自旋,很浪费 CPU。
当某个线程执行任务的时间非常长,自旋的时间就会非常长,长时间的自旋也是很浪费 CPU 的。所以当第二个线程自旋一定次数之后,还是没拿到锁或者有更多线程来抢锁的时候,就得直接进入重量级锁了,把那些没拿到锁的线程都给阻塞住。
当升级到重量级锁的时候,对象头的 Mark word 的指针就会指向锁监视器。为什么要有锁监视器?锁监视器主要是用来负责记录锁的拥有者,记录锁的重入次数,负责线程的阻塞唤醒。准确来说,锁监视器就是一个对象,它有这么几个字段。
・首先是 owner,用来保存持有锁的线程。
・然后重入次数的计数器,用来计入锁的重入次数。
・然后还有锁池和等待池。锁池只要用来管理抢锁失败的线程,等待池主要用来管理调用 wait 方法,陷入等待状态的线程。
・重入次数计数器是干嘛的?当一个线程重复的去获取这个锁,这就是可重入锁。
・synchronized 的可重入怎么实现?就是依赖锁监视器内部重入次数的计数器,重入一次计数器加一次,释放一次计数器减一次减到零,就完全释放锁了。
锁池和等待池是干嘛的?

首先在重量级锁状态,当有线程拿到锁,此时监视器的 owner 字段就记录拿到锁的线程,没有拿到锁的线程就被阻塞住,进入 blocking 状态,然后放到锁池中。
当拿到锁的线程调用了 wait 方法,那该线程就释放锁,然后进入 waiting 状态,然后被放到等待池当中。当某个线程调用了 notify,唤醒了这个 waiting 的线程,那这个线程就从 waiting 的状态变成 blocking 状态,然后再被放入到锁池中,等待锁释放,重新去抢锁。这就是锁池和等待池的作用。
锁竞争失败的线程和调用了 wait 方法的线程,有什么本质区别吗?这不都是在阻塞等待吗?为什么要放在锁池和等待池这两个完全不同的集合中?
首先锁池放的是竞争锁失败的线程,线程状态是 blocking,竞争锁失败,那他的目标就是要尽快获取锁去执行任务,这是锁的互斥问题。等待池放的是主动放弃锁的线程,我主动放弃了锁,我暂时还不想要锁。这个线程等待被其他线程唤醒之后,去配合其他线程去完成某项任务的,线程状态是 waiting 或者 time waiting。这些 waiting 状态的线程,是想等某个资源到位了,等某个事情完成了,然后再被 notify 唤醒,然后放入锁池中,准备去抢锁做业务。这是线程通信问题,所以等待池的线程和锁池的线程,它的目标和要解决的问题完全不一样。当然要放到两个不同的集合了。

http://www.dtcms.com/a/318436.html

相关文章:

  • JAVA--流程控制语句
  • Android—服务+通知=>前台服务
  • shell基础之EOF的用法
  • 译 | 在 Python 中从头开始构建 Qwen-3 MoE
  • windos安装了python,但是cmd命令行找不到python
  • 012 网络—基础篇
  • 机器学习算法系列专栏:逻辑回归(初学者)
  • flex布局:容器的justify-content属性
  • Python训练Day35
  • Python在生物计算与医疗健康领域的应用(2025深度解析)
  • 局域网内某服务器访问其他服务器虚拟机内相关服务配置
  • 无人机遥控器舵量技术解析
  • 线上Linux服务器的优化设置、系统安全与网络安全策略
  • Android14的QS面板的加载解析
  • 云平台托管集群:EKS、GKE、AKS 深度解析与选型指南-第四章
  • k8s 网络插件 flannel calico
  • 第14届蓝桥杯Scratch选拔赛初级及中级(STEMA)真题2023年1月15日
  • 链式数据结构
  • LangChain4j实战
  • 深入解析系统调试利器:strace 从入门到精通
  • Linux——(16)深入理解程序运行的基石
  • 12. SELinux 加固 Linux 安全
  • react 流式布局(图片宽高都不固定)的方案及思路
  • npm run dev npm run build
  • Activiti7 调用子流程的配置和处理
  • 【Day 17】Linux-SSH远程连接
  • TMS320F2837xD的CLA加速器开发手册
  • mobaxterm怎么复制全局内容
  • ABP VNext + SQL Server Temporal Tables:审计与时序数据管理
  • 串口通信 day48