java线程内存模型底层实现原理

简介: java线程内存模型底层实现原理

一、多核并发缓存架构

在计算机里面有多个cpu和主内存,早期的计算机只有主内存和cpu。cpu要读取数据,而数据一般在硬盘上的,一开始是先把数据读取到主内存,然后再cpu和主内存进行交互,去拿些数据,或者说再和这些数据做些运算。早期的计算机是cpu和主内存直接打交道的。这么多年的发展,cpu的计算速度是非常快的。在摩尔定律里面。cpu每隔18个月左右,它的运算速度会提升很多,但是主内存的读取数据的速度,和存储数据的速度并不大。随着cpu的高速提升,然后cpu和主内存的数据的交换肯定有性能瓶颈的,一直会卡在主内存,为了解决这个问题,然后再他俩之间引入了cpu缓存,cpu寄存器也可以看成是cpu缓存。cpu缓存的速度和cpu的速度是很接近的。读取数据,就是和cpu缓存频繁打交道的。

一、java线程内存模型

1、概念:

Java线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽了底层不同计算机的区别,严格的讲java内存模型是Java线程内存模型

比如在上面的图中,多个线程同时运行程序,如果是多核cpu的话,可能是一个线程利用一个一核cpu,比如一些共享变量是存储到主内存里面的。而线程不会频繁的和主线程做交互,而是把主内存里面的共享变量复制一份到工作内存里面。所以线程运行程序的时候是和工作内存频繁的在做交互,这样性能会很高,同时线程B,C也是这样。

举个例子,代码如下:

  1. public class VolatileVisibility {
  2.    //共享变量
  3.    private  static  boolean  initFlag=false;

  4.    public static void main(String[] args) throws InterruptedException {
  5.        new Thread(new Runnable() {
  6.            public void run() {
  7.                System.out.println("waiting  data.....");
  8.                while(!initFlag){

  9.                }
  10.                System.out.println("-------------------------success");
  11.            }

  12.        }).start();
  13.        Thread.sleep(2000);
  14.        new Thread(new Runnable() {
  15.            public void run() {
  16.                prepareData();
  17.            }
  18.        }).start();
  19.    }
  20.    public  static  void  prepareData(){
  21.        System.out.println("prepareing data.....");
  22.        initFlag=true;
  23.        System.out.println("prepare data end.....");
  24.    }

  25. }

运行的结果如下:第一个线程没有结束

解释:上面的initFlag:也就是上面图中的共享变量。之前是等于false。这两个线程是会同时进行操作这个变量,把这个共享变量分别加载到自己的工作内存从而操作,所以一开始在两个线程的工作内存上都为false,第二个线程把initFlag改为true了。而第一个线程是没有感知的,因为改的是第二个变量副本和主内存中的共享变量,第一个线程没有改自己的变量副本的。

如何去修改上面的死循环呢?

在共享变量上加上一个volatile关键字就可以了:保证多线程在操作共性变量的可见性。这样答问题的话是太入门了,下面会说底层的原理,怎么达到可见性的。

想要把volatile搞明白的话,必须还要把java内存模型搞明白:

二、java内存模型:

java内存模型除了上面的图的以后还定义了一些原子操作,程序的运行是按照上面的图运行走的。

1、java中的线程原子操作:

read:就是把主内存的共享变量读取出来,也就是对应下面的操作

use:线程1操作做的就是取反操作

两个线程,线程1等待数据的操作,线程2准备数据的操作:

1、首先主内存的值为false,随着程序的运行,第一个线程开始执行的时候把主内存的变量加载到工作内存的里面:会经历read操作,把主内存的数据给 读取出来。

2、load把数据变量读取到工作内存里面。

3、use:做的是取反操作

上面的就是线程1的操作。

第二个线程就是把主内存的数据读取后来然后再load下,这时把变量放到工作内存里面取了。

上面的图就是解释没有加volatile关键字的程序之前的效果。

而加了volatile关键字之后早期底层实现:

加了volatile关键字之后,两个线程之间的工作内存的副本变量就是可见的,就是达到同步数据的效果,早期的硬件级别使用的是总线加锁的效果。

总线:学过计算机组成原理的都听说过总线的,比如说主内存和工作内存是通过总线来操作的,cpu和主内存交互,在物理硬件中是cpu在一个地方,主内存在另一个地方。两个是通过一排一排的线连接的。就可以把这个线当作成总线。数据的传输是通过总线来传输的。早期的volatile也解决了共享变量一致性的问题,也就是没有加锁之前这两个线程是并行在执行的。

加锁之后就把并行的操作变成串行的操作了。

A、总线加锁(性能太低)

cpu从主主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他cpu没法去读或写这个数据,直到这个cpu使用完成数据释放锁之后其他cpu才能读取该数据。

下面的图就是早期的volatile关键字的底层情况,底层实现并不一定是代码实现的,而是硬件实现的。

在上面的图中,早期的操作是在read的过程之前做一个lock的操作,其他的线程通过总线拿主内存中的数据的话,这时发现数据已经加上一把锁了,这时是拿不到数据的,这时的锁可能为写锁啊,读锁啊,暂时先不管,直到在线程2所有的操作执行完执行unlock操作释放锁之后,其他的线程才可以再来拿数据,而其他线程可能涉及到锁的争抢。而拿到值的线程最先肯定要加锁。最新拿到锁的线程就能够拿到新的值了,其他的线程就需要等待。这就可以解决早期的共享变量的一致性的问题。

没有加锁之前,两个线程是完全并行再执行的。但是加完锁之后,可能一开始还是并行在执行的,但是这两个线程读取到同一个变量的时候,他们之间可能就争抢锁。争抢锁可能就需要排队。把完全并行的操作就变成串行的操作了。

这种早期总线加锁的效率降低。这种很多时候不是java代码实现的,而是硬件去实现的。

而现在java底层对volatile关键字是怎么实现的呢?

先说下MESI缓存一致性协议

volatile的底层实现就是借助于MESI缓存一致性协议实现的

B、MESI缓存一致性协议:

多个cpu从主内存读取同一个数据到各自的告诉缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己的缓存里的数据失效。

volatile底层的实现大概就是这么操作。

在多线程执行的时候会把共享变量放入到自己的工作内存,当其中的某个cpu修改了缓存中的副本变量的值,如果线程2把这个值给修改了,这个线程只要把这个值同步到主内存的话,其实store的操作就是把这个值写入到主内存了,然后通过write操作写入到主内存的对象里面去。当cpu启动MESI缓存一致性协议被启动之后,当线程2通过store的操作的时候,通过总线的一刹那。然后线程1的启动cpu总线嗅探机制就会感知总线的关键字很敏感;initFlag,并且把自己本身的工作内存的原本的值给失效。然后线程1所在的cpu发现这个变量的地址已经失效了。然后线程1重新去主内存read操作和load操作,这时true取反跳出了while的循环,这就把可见性的问题给解决了。线程之间无法进行数据传输,必须通过主内存。这就是通过MESI缓存一致性协议来解决共享变量可见性的问题。

上面的就是volatile的底层的一个最简单的实现,volatile关键字借助了MESI缓存一致性协议实现了,其实还是借助更多的来实现的,比如加锁的实现:底层是用c语言来实现的。

下面提出两个问题:

1、如果两个子线程同时回写主内存的话,是个问题,

2、如果其中一个线程没有同步到主内存的时候,另一个线程的cpu总线嗅探机制已经监听到initFlag的值有变化之后,然后把自己的工作内存中的initFlag失效。这时线程1读的数据还是false,这又是一个问题。所以并发编程不是那么简单的

由下篇文章揭晓答案,明天继续去写深入汇编语言来分析volatile关键字。

相关文章
|
6天前
|
缓存 Java 应用服务中间件
Java虚拟线程探究与性能解析
本文主要介绍了阿里云在Java-虚拟-线程任务中的新进展和技术细节。
|
3天前
|
Java 编译器
深入理解Java内存模型:从基础到高级
本文旨在通过通俗易懂的方式,引导读者深入理解Java内存模型(JMM)的核心概念和工作原理。我们将从简单的基础知识入手,逐步探讨重排序、顺序一致性问题以及volatile关键字的实现机制等高级主题。希望通过这篇文章,你能够对Java内存模型有一个清晰、全面的认识,并在实际编程中有效地避免并发问题。
|
1天前
|
存储 算法 Java
深入理解Java内存管理
本文将通过通俗易懂的语言,详细解析Java的内存管理机制。从JVM的内存结构入手,探讨堆、栈、方法区等区域的具体作用和原理。进一步分析垃圾回收机制及其调优方法,最后讨论内存泄漏的常见场景及防范措施。希望通过这篇文章,帮助读者更好地理解和优化Java应用的内存使用。
|
3天前
|
Java 开发者
Java中的多线程基础与应用
【9月更文挑战第22天】在Java的世界中,多线程是一块基石,它支撑着现代并发编程的大厦。本文将深入浅出地介绍Java中多线程的基本概念、创建方法以及常见的应用场景,帮助读者理解并掌握这一核心技术。
|
5天前
|
Java
领略Lock接口的风采,通过实战演练,让你迅速掌握这门高深武艺,成为Java多线程领域的武林盟主
领略Lock接口的风采,通过实战演练,让你迅速掌握这门高深武艺,成为Java多线程领域的武林盟主
21 7
|
4天前
|
Java 程序员
Java中的多线程基础与实践
【9月更文挑战第21天】本文旨在引导读者深入理解Java多线程的核心概念,通过生动的比喻和实例,揭示线程创建、同步机制以及常见并发工具类的使用。文章将带领读者从理论到实践,逐步掌握如何在Java中高效地运用多线程技术。
|
2天前
|
Java 调度 开发者
Java中的多线程编程:从基础到实践
本文旨在深入探讨Java多线程编程的核心概念和实际应用,通过浅显易懂的语言解释多线程的基本原理,并结合实例展示如何在Java中创建、控制和管理线程。我们将从简单的线程创建开始,逐步深入到线程同步、通信以及死锁问题的解决方案,最终通过具体的代码示例来加深理解。无论您是Java初学者还是希望提升多线程编程技能的开发者,本文都将为您提供有价值的见解和实用的技巧。
9 2
|
5天前
|
监控 算法 Java
Java中的内存管理与垃圾回收机制
本文将深入探讨Java编程语言中的内存管理方式,特别是垃圾回收(Garbage Collection, GC)机制。我们将了解Java虚拟机(JVM)如何自动管理内存,包括对象创建、内存分配以及不使用对象的回收过程。同时,我们还将讨论不同的垃圾回收算法及其在不同场景下的应用。
|
4天前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
4天前
|
Java 数据处理
Java中的多线程编程:从基础到实践
本文旨在深入探讨Java中的多线程编程,涵盖其基本概念、创建方法、同步机制及实际应用。通过对多线程基础知识的介绍和具体示例的演示,希望帮助读者更好地理解和应用Java多线程编程,提高程序的效率和性能。
16 1