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关键字。

相关文章
|
8天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
4天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
23 9
|
8天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
29 6
|
7天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
7天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
21 3
|
6天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
7天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
17 1
|
7天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
8天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
33 1