浅析 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 得到了优化,新增了几种锁,以及不同情况下的状态变化,以避免直接重量级锁产生的性能损耗。

目录
相关文章
|
2天前
|
安全 Java 编译器
是时候来唠一唠synchronized关键字了,Java多线程的必问考点!
本文简要介绍了Java中的`synchronized`关键字,它是用于保证多线程环境下的同步,解决原子性、可见性和顺序性问题。从JDK1.6开始,synchronized进行了优化,性能得到提升,现在仍可在项目中使用。synchronized有三种用法:修饰实例方法、静态方法和代码块。文章还讨论了synchronized修饰代码块的锁对象、静态与非静态方法调用的互斥性,以及构造方法不能被同步修饰。此外,通过反汇编展示了`synchronized`在方法和代码块上的底层实现,涉及ObjectMonitor和monitorenter/monitorexit指令。
12 0
|
7天前
|
安全 Java 开发者
Java并发编程:深入理解Synchronized关键字
【4月更文挑战第19天】 在Java多线程编程中,为了确保数据的一致性和线程安全,我们经常需要使用到同步机制。其中,`synchronized`关键字是最为常见的一种方式,它能够保证在同一时刻只有一个线程可以访问某个对象的特定代码段。本文将深入探讨`synchronized`关键字的原理、用法以及性能影响,并通过具体示例来展示如何在Java程序中有效地应用这一技术。
|
8天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
9天前
|
Java
浅谈Java的synchronized 锁以及synchronized 的锁升级
浅谈Java的synchronized 锁以及synchronized 的锁升级
8 0
|
11天前
|
存储 缓存 Java
线程同步的艺术:探索 JAVA 主流锁的奥秘
本文介绍了 Java 中的锁机制,包括悲观锁与乐观锁的并发策略。悲观锁假设多线程环境下数据冲突频繁,访问前先加锁,如 `synchronized` 和 `ReentrantLock`。乐观锁则在访问资源前不加锁,通过版本号或 CAS 机制保证数据一致性,适用于冲突少的场景。锁的获取失败时,线程可以选择阻塞(如自旋锁、适应性自旋锁)或不阻塞(如无锁、偏向锁、轻量级锁、重量级锁)。此外,还讨论了公平锁与非公平锁,以及可重入锁与非可重入锁的特性。最后,提到了共享锁(读锁)和排他锁(写锁)的概念,适用于不同类型的并发访问需求。
40 2
|
11天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
2天前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
19 0
|
1天前
|
缓存 Java
【Java基础】简说多线程(上)
【Java基础】简说多线程(上)
5 0
|
1天前
|
并行计算 算法 安全
Java从入门到精通:2.1.3深入学习Java核心技术——掌握Java多线程编程
Java从入门到精通:2.1.3深入学习Java核心技术——掌握Java多线程编程
|
2天前
|
监控 安全 Java
在Java中如何优雅的停止一个线程?可别再用Thread.stop()了!
在Java中如何优雅的停止一个线程?可别再用Thread.stop()了!
8 2