浅析 synchronized 底层实现与锁相关 | Java(上)

简介: 一切的最开始都是源自为什么?

引言

一切的最开始都是源自为什么?

  • 为什么加了锁 synchronized 关键字,就可以实现同步?
  • synchronized 底层到底做了什么优化?
  • Java 中的各种锁及锁膨胀?
  • 用户态、内核态与上下文切换到底是什么鬼?
  • 什么叫自旋锁,它与 CAS 的关系?
  • 对象头是什么玩意,什么又是 MarkWord ?

概述

synchronizrd 是开发中解决同步问题中最常见,也是最简单的一种方法。从最开始学习并发编程,我们都知道,只要加上这个 synchronizrd 关键字,就可以很大程度上轻松解决同步问题。相应的,从原理上来讲,其也是比较重的一种操作,特别是 jdk1.5 时候,相比 JUC 中的 Lock 锁,一定程度上逊色不少。但随着jdk1.6对 synchronized 的优化后,synchronizrd 并不会显得那么重,相比使用 Lock 而言,其的性能大多数情况下也可以接近 Lock 。

本文的主旨就是对 synchronized 的原理进行探秘,从而完成对各种锁的了解与学习。

synchronizrd 常用的作用有三个:

原子性

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行;

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值;

有序性

防止编译器和处理对指令进行重排序,即也就是抑制指令重排序,;

解析

synchronziedjvm 里的实现都是基于进入和退出 Monitor 对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过 成对 的 MonitorEnterMonitorExit 指令来实现。具体如下图对比:

示例代码



字节码对比:



同步方法 ,从上图字节码来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来实现,而是直接在方法中增加了 synchronzied 修饰。更底层实现上而言,其常量池中多了 ACC_SYNCHRONIZED 标识符,JVM 就是根据该标识符来实现方法的同步:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor ,获取成功之后才能执行方法体,方法执行完后再释放 monitor 。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。

对于 同步代码块 而言,synchronzied 的底层实现中,MonitorEnter 指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象 Monitor 的所有权,即尝试获得该对象的锁,而 monitorExit 指令则插入在方法结束处和异常处,JVM 保证每个 MonitorEnter 必须有对应的 MonitorExit 。

在上面,说到了 synchronized 的在字节码上的实现,那对于虚拟机而言,synchronzied 锁的标志到底放哪了呢?说到这个问题,我们不得不提 对象头 这个概念。

对象头

什么是对象头,对象头干了什么?

如果看过垃圾回收机制,那么可能知道这个玩意。对象头其就相当于一个名片,它包含了对象的一个基本信息,如下图所示:


网络异常,图片无法展示
|


注意:对象头中不一定包含数组长度,如果这个对象不是数组 ✋

整个对象头由两个部分组成,分别是 KlassPointMark Word

KlassPoint

当我们 new 出一个类时,虚拟机如何得知它是哪个类呢, 这时候就是通过上述 KlassPoint ,其指向了类元数据 (mteaData) 的信息。

元数据

在计算机中,有各种 [元] 数据。比如文件有元数据,网页有元标签。 这个说法来自希腊语,表示关于。所以 文件中的元数据,即为关于文件的数据,类的元数据即为类信息的一个原始标签。也即为描述这个类的信息

Mark Word

用于存储不同状态信息,是会随着时间点而改变。一般而言默认数据是存储对象的 HashCode 等信息,而我们本篇的主题 synchronzied 正是在其里存储。如下图所示

image.png


Markword的信息会随着时间不断改变,比如发生gc时,内部gc 标记为null。



而我们本篇的主题 synchronized 的 锁状态 也存在与 MarkWord 中,在对象运行变化的过程中,锁的状态存在4种变化状态,即 无锁状态 、偏向锁状态 、轻量级锁状态 、重量级锁状态 。它会随着竞争情况逐渐升级,锁可以升级但不能降级,主要目的是为了提高获得锁和释放锁的效率。

那为什么要存在着几种🔐呢?或者还没看明白缘由?请接着下面继续看?👇

上下文切换

在jdk1.6之后,synchronizrd 得到了优化,而添加各种锁的目的都是为了避免直接加锁而导致的上下文切换从而引发的耗时浪费。

如果不了解上下文切换,这样说可能听着有点懵,我们先从基础讲一下:

什么是上下文切换?

1.在单处理器时期,操作系统就能处理多线程并发任务,处理器给每个线程分配CPU时间片,线程在CPU时间片内执行任务

  • CPU时间片是CPU分配给每个线程执行的时间段,一般为几十毫秒

2.时间片决定了一个线程可以连续占用处理器运行的时长

  • 当一个线程的时间片用完,或者因自身原因被迫暂停运行,此时另一个线程会被操作系统选中来占用处理器
  • 上下文切换(Context Switch):一个线程被暂停剥夺使用权,另一个线程被选中开始或者继续运行的过程
  • 切出:一个线程被剥夺处理器的使用权而被暂停运行
  • 切入:一个线程被选中占用处理器开始运行或者继续运行
  • 切出切入的过程中,操作系统需要保存和恢复相应的进度信息,这个进度信息就是*上下文*

3. 上下文的内容

  • 寄存器的存储内容:CPU寄存器负责存储已经、正在和将要执行的任务
  • 程序计数器存储的指令内容:程序计数器负责存储CPU正在执行的指令位置、即将执行的下一条指令的位置

4.当CPU数量远远不止1个的情况下,操作系统将CPU轮流分配给线程任务,此时的上下文切换会变得更加频繁

  • 并且存在跨CPU的上下文切换,更加昂贵

所以,当我们某个资源使用 synchronizrd 进行加锁时:

  1. 当线程A获取了锁,线程B在获取时将会被阻塞,也即是 BLOCKED 状态,此时线程B暂停被操作系统 切出 ,操作系统会保存此时的上下文;
  2. 当线程A释放了锁,此时假设线程B获取到了锁,线程B 从 BLOCKED 进入 RUNNABLE 状态,即线程重新唤醒,此时线程将获取上次操作系统保存的上下文继续执行。

上述的过程中线程B执行了 两次 上下文切换,每一次上下文切换的过程为 3~5微秒 ,而cpu执行一条指令只需要 0.6ns ,所以如果加锁后只是执行几条普通指令,如某个变量的自增或者其他,那么上下文切换将对性能产生极大影响,所以在jdk1.6以后,synchronizrd 得到了优化,新增了几种锁,以及不同情况下的状态变化,以避免直接重量级锁产生的性能损耗。

目录
相关文章
|
8天前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
10天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
38 4
|
30天前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
|
1月前
|
Java
让星星⭐月亮告诉你,Java synchronized(*.class) synchronized 方法 synchronized(this)分析
本文通过Java代码示例,介绍了`synchronized`关键字在类和实例方法上的使用。总结了三种情况:1) 类级别的锁,多个实例对象在同一时刻只能有一个获取锁;2) 实例方法级别的锁,多个实例对象可以同时执行;3) 同一实例对象的多个线程,同一时刻只能有一个线程执行同步方法。
18 1
|
30天前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
15 0
|
1月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
31 0
|
6天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
15天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
2天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
15 9
|
5天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####