博客搬家啦,更多干货 https://blog.csdn.net/qq_2816...

一些基础概念:

现代计算机硬件结构原理图

image.png

cpu 与内存的交互模型

image.png

CPU读取存储器数据过程

  • CPU要取寄存器的值,只需要一步:直接读取。
  • CPU要取L1 cache的某个值,需要1-3步(或者更多):把cache行锁住,把某个数据拿来,解锁,如果没锁住就慢了。
  • CPU要取L2 cache的某个值,先要到L1 cache里取,L1当中不存在,在L2里,L2开始加锁,加锁以后,把L2里的数据复制到L1,再执行读L1的过程,上面的3步,再解锁。
  • CPU取L3 cache的也是一样,只不过先由L3复制到L2,从L2复制到L1,从L1到CPU。
  • CPU取内存则最复杂:通知内存控制器占用总线带宽,通知内存加锁,发起内存读请求,等待回应,回应数据保存到L3(如果没有就到L2),再从L3/2到L1,再从L1到CPU,之后解 除总线锁定。

多线程环境下存在的问题

缓存一致性问题 在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存
(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是 也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一 块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步 回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都 遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、 MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等

image.png

指令重排序问题

为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执 行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该 结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的 顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不 能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有 类似的指令重排序(Instruction Reorder)优化
简单来讲,单线程指令重排不会有任何问题,多线程会有

用户态和内核态

Linux为内核代码和数据结构预留了几个页框,这些页永远不会被转出到磁盘上。从 0x00000000 到 0xc0000000(PAGE_OFFSET) 的线性地址可由用户代码 和 内核代码进行 引用(即用户空间)。从0xc0000000(PAGE_OFFSET)到 0xFFFFFFFFF的线性地址只能由 内核代码进行访问(即内核空间)。内核代码及其数据结构都必须位于这 1 GB的地址空间 中,但是对于此地址空间而言,更大的消费者是物理地址的虚拟映射。
这意味着在 4 GB 的内存空间中,只有 3 GB 可以用于用户应用程序。一个进程只能运行 在用户方式(usermode)或内核方式(kernelmode)下。用户程序运行在用户方式下,而

系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆 栈,而内核方式下用的是固定大小的堆栈(一般为一个内存页的大小)

用户线程和内核线程

  • 用户线程

指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应 用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。另外,用户线程 是由应用进程利用线程库创建和管理,不依赖于操作系统核心。不需要用户态/核心态切换, 速度快。操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所 有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间 相对减少。

  • 内核线程

线程的所有管理操作都是由操作系统内核完成的。内核保存线程的状态和上下 文信息,当一个线程执行了引起阻塞的系统调用时,内核可以调度该进程的其他线程执行。在 多处理器系统上,内核可以分派属于同一进程的多个线程在多个处理器上运行,提高进程执行 的并行度。由于需要内核完成线程的创建、调度和管理,所以和用户级线程相比这些操作要慢 得多,但是仍然比进程的创建和管理操作要快。大多数市场上的操作系统,如Windows, Linux等都支持内核级线程。

image.png

由图所示,用户级线程内核无感知,不由内核调度,内核级线程在内核空间里会有线程表一一记录

java8里所创建的线程是内核级线程

image.png

什么是JMM模型?

Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描 述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构 成数组对象的元素)的访问方式。JVM运行程序的实体是线程,而每个线程创建时JVM都会为 其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规 定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的 操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空 间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量, 工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区 域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成

线程,工作内存,主内存 交互图(基于jmm规范)

image.png

Java内存模型与硬件内存架构的关系

通过对前面的硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该 已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行,但Java内存模型和硬件内 存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作 内存(线程私有数据区域)和主内存(堆内存)之分,也就是说Java内存模型对内存的划分对硬件内 存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作 内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可 能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一 个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。

JMM存在的必要性

在明白了Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具 体关系后,接着来谈谈Java内存模型存在的必要性。由于JVM运行程序的实体是线程,而每个 线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数 据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量从主内存拷贝 的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,如 果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。

JMM-同步八种操作介绍

  1. lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的 变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中, 以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作 内存的变量副本中
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存 的变量
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中, 以便随后的write的操作
  8. write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送 到主内存的变量中

如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作, 如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内 存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

image.png

并发编程的可见性,原子性与有序性问题

  • 原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会 被其他线程影响。

X=10; //原子性(简单的读取、将数字赋值给变量)
Y = x; //变量之间的相互赋值,不是原子操作
X++; //对变量进行计算操作
X = x+1;
  • 可见性

理解了指令重排现象后,可见性容易了,可见性指的是当一个线程修改了某个共享变量的 值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,因为 我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改 过的新值。
但在多线程环境中可就不一定了。

  • 有序性

有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样 的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未 必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保 证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。

可见性问题

volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被 其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。

volatile内存语义

volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用

  • 保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
  • 禁止指令重排序优化。

volatile无法保证原子性

volatile禁止重排优化

内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行 顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译 器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器 和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏 障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之, volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。

那么整体流程是这样的。

image.png

volatile 和 mesi没关系

mesi解决cpu缓存和主存的问题,volatile解决工作内存于主内存的关系,不搭嘎

被volatile关键字修饰的变量,在每个写操作之后,都会加入一条store内存屏障命令,此命令强制工作内存将此变量的最新值保存至主内存;在每个读操作之前,都会加入一条load内存屏障命令,此命令强制工作内存从主内存中加载此变量的最新值至工作内存。


Devilu
85 声望4 粉丝

just a newbie