史上最全的Java并发系列之Java并发机制的底层实现原理

简介: 前言文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…种一棵树最好的时间是十年前,其次是现在

絮叨


昨天从大的方向上介绍了Java并发的一个全局观,了解了JDK的JUC,那么今天我们从最底层的原理来探索这些并发,这也是面试问的最多的地方之一,问底层,如果能理解当然是好的啦,前面的内容在下面的链接:

Java代码 编译之后 得到 Java字节码,被 类加载器加载到JVM中,最终 转化为汇编指令。Java中的并发机制依赖于JVM的实现和CPU的指令

并发编程的3个基本概念


原子性

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

原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

a. 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。


b.所有引用reference的赋值操作


c.java.concurrent.Atomic.* 包中所有类的一切操作


可见性

定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。


在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。


有序性

定义:即程序执行的顺序按照代码的先后顺序执行。

Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。


锁的互斥和可见性


锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。

  • 互斥即一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据。
  • 可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。


Java的内存模型JMM以及共享变量的可见性


JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。


需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存


存储器的内存结构


每次读一个数据都是先从上一直找到下 如果cpu高速缓存 1 2有的话,那么速度会快很多,越靠近CPU的地方 速度越快 成本越高


如果是写入一个数据,它也会一层一层的写到各个缓存中去 (这里还涉及到一个高级概念 缓存行 就是CPU做高速缓存的时候 他并不是说一个字节去缓存的,而是一行去缓存,并且这个大小是64个字节 所以JDK源码 中 为了避免 缓存行里面其他数据的更新,你可以定义64个字节的数据,去做缓存,可能比你做一个8个字节的要快)


Volatile原理


Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。


在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。因为它不会引起线程上下文的切换和调度


当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。


而声明变量是 volatile 的,JVM 保证了每次读变量都从JMM中读,跳过 CPU cache(线程的本地缓存) 这一步。


volatile是如何保证可见性的呢

把有volatile关键字修改的代码 变成汇编代码的时候发现,它的代码前面多了一个lock 关键字,这个前缀的指令在多核处理器下会引发2件事情

  • 将当前处理器缓存行的数据回写到系统内存中
  • 这个回写操作,会导致其他线程的本地缓存无效(内部是通过缓存一致性协议,通过在总线上的传播数据来检查自己的缓存是否有效)
  • MESI Cache 缓存一致性协议
  • Modified 修改的
  • Exclusive 独占的
  • Shared 共享的
  • Invalid 无效的


synchronized

在多线程中,synchronized 一直是一个元老级别的角色,很多人会称呼他为重量级的锁,但是1.6对它的优化之后,并不那么重量了。


synchronized实现同步

Java中的每个对象都可以作为锁,它有以下三种表现形式

  • 对于 普通同步方法,锁是 当前实例对象。
  • 对于 静态同步方法,锁是 当前类的Class对象。
  • 对于 同步方法块,锁是 Synchonized括号里配置的对象(可能是实例对象。也可能是Class对象)。


Java对象头

synchronized用的锁是存在Java对象头里的。在32位 虚拟机中,1字宽 等于4字节,即32bit。

  • 数组类型,虚拟机用3个字宽存储对象头。
  • 非数组类型,虚拟机用2字宽存储对象头。

我们知道的Java的锁是存在每个对象里面,那具体是存在哪里呢?

在Java对象头里面有一个叫 Mark Word的区域,里面存着HashCode 分代年龄,锁标记位。


锁的4种状态


级别从低到高依次是:

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

这边说一下偏向锁的原理吧?自己总结的也不一定对,就是说当一个线程去获得一个偏向锁要走的几步

  • 第一步,先判断再对象头里面是否存储了当前线程的id和判断一下锁标志位的状态,
  • 第二步,如果是有当前线程id,就直接省去了CAS操作来加锁,解锁,如果没有则就行下一步
  • 第三步,通过CAS 获得锁,然后把当前线程id存到对象头里面,然后把同步代码块执行完成,第四步就是接下来,要释放偏下锁
  • 第四步,偏向锁的释放机制是当有下一个线程来竞争锁的时候,发现CAS不成功,那么就释放锁,然后再去竞争锁

至于 轻量级锁 我认为就是还是处于自旋 线程还没挂起的状态


原子操作的实现原理


原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。

处理器如何实现原子操作

  • 使用总线锁保证原子性:所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
  • 使用缓存锁保证原子性:这个的意思是在每个线程的本地缓存中,我不会去管你,但是最后会写到内存中的时候,我会用缓存一致性原理让你只能有一个线程能回写内存成功,然后告知其他线程的本地缓存失效,让他们重新去更新本地缓存,再去操作,

以下两种情况不会使用缓存锁:

  • 当处理器不支持缓存锁定。
  • 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。


Java如何实现原子操作

Java 提供了2种原子性的方法

  • Java使用锁来保证原子性操作,锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。
  • 使用循环CAS实现原子性操作

什么是CAS?

在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。操作结果必须说明是否进行替换;这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成(摘自维基本科)

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。至于底层是unsafe包实现的 里面是调用的native方法(底层c++实现),操作cpu的,这边就不往下深入了,不是不想,是博主太菜了


CAS操作的三大问题

  • ABA问题,这个是最常见的问题之一了,这个也简单就是A变成了B 最后变成了A,那么内存值 和预期值是相当的,所以他认为这个操作是原子的,其实不是,
  • CAS是循环的去操作,如果长时间不成功,对于cou的消耗比较大
  • 只能保证对于一个共享变量的原子性操作,如果是多个建议用锁

结尾


第二章,介绍了并发机制的底层实现原理,valatile synchronized的实现原理,CAS 的优缺点,原子性问题等,后面的很多东西,但是要基于这个来实现的,今天就到这了吧

因为博主也是一个开发萌新 我也是一边学一边写 我有个目标就是一周 二到三篇 希望能坚持个一年吧 希望各位大佬多提意见,让我多学习,一起进步。


相关文章
|
设计模式 人工智能 安全
AQS:Java 中悲观锁的底层实现机制
AQS(AbstractQueuedSynchronizer)是Java并发包中实现同步组件的基础工具,支持锁(如ReentrantLock、ReadWriteLock)和线程同步工具类(如CountDownLatch、Semaphore)等。Doug Lea设计AQS旨在抽象基础同步操作,简化同步组件构建。 使用AQS需实现`tryAcquire(int arg)`和`tryRelease(int arg)`方法以获取和释放资源,共享模式还需实现`tryAcquireShared(int arg)`和`tryReleaseShared(int arg)`。
554 32
AQS:Java 中悲观锁的底层实现机制
|
人工智能 Java 关系型数据库
Java——SPI机制详解
SPI(Service Provider Interface)是JDK内置的服务提供发现机制,主要用于框架扩展和组件替换。通过在`META-INF/services/`目录下定义接口实现类文件,Java程序可利用`ServiceLoader`动态加载服务实现。SPI核心思想是解耦,允许不同厂商为同一接口提供多种实现,如`java.sql.Driver`的MySQL与PostgreSQL实现。然而,SPI存在缺陷:需遍历所有实现并实例化,可能造成资源浪费;获取实现类方式不够灵活;多线程使用时存在安全问题。尽管如此,SPI仍是Java生态系统中实现插件化和模块化设计的重要工具。
712 0
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
368 0
|
10月前
|
人工智能 前端开发 安全
Java开发不可不知的秘密:类加载器实现机制
类加载器是Java中负责动态加载类到JVM的组件,理解其工作原理对开发复杂应用至关重要。本文详解类加载过程、双亲委派模型及常见类加载器,并介绍自定义类加载器的实现与应用场景。
380 4
|
人工智能 JavaScript Java
Java反射机制及原理
本文介绍了Java反射机制的基本概念、使用方法及其原理。反射在实际项目中比代理更常用,掌握它可以提升编程能力并理解框架设计原理。文章详细讲解了获取Class对象的四种方式:对象.getClass()、类.class、Class.forName()和类加载器.loadClass(),并分析了Class.forName()与ClassLoader的区别。此外,还探讨了通过Class对象进行实例化、获取方法和字段等操作的具体实现。最后从JVM类加载机制角度解析了Class对象的本质及其与类和实例的关系,帮助读者深入理解Java反射的工作原理。
306 0
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
430 0
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
289 1
|
缓存 安全 Java
【高薪程序员必看】万字长文拆解Java并发编程!(3-1):并发共享问题的解决与分析
活锁:多个线程相互影响对方退出同步代码块的条件而导致线程一直运行的情况。例如,线程1的退出条件是count=5,而线程2和线程3在其代码块中不断地是count进行自增自减的操作,导致线程1永远运行。内存一致性问题:由于JIT即时编译器对缓存的优化和指令重排等造成的内存可见性和有序性问题,可以通过synchronized,volatile,并发集合类等机制来解决。这里的线程安全是指,多个线程调用它们同一个实例的方法时,是线程安全的,但仅仅能保证当前调用的方法是线程安全的,不同方法之间是线程不安全的。
218 0
|
Java 程序员
【高薪程序员必看】万字长文拆解Java并发编程!(3-2):并发共享问题的解决与分析
wait方法和notify方法都是Object类的方法:让当前获取锁的线程进入waiting状态,并进入waitlist队列:让当前获取锁的线程进入waiting状态,并进入waitlist队列,等待n秒后自动唤醒:在waitlist队列中挑一个线程唤醒:唤醒所有在waitlist队列中的线程它们都是之间协作的手段,只有拥有对象锁的线程才能调用这些方法,否则会出现IllegalMonitorStateException异常park方法和unpark方法是LockSupport类中的方法。
220 0
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####