详细分析Java内存模型

目录
  • 一、为什么要学习并发编程
  • 二、为什么需要并发编程
  • 三、从物理机中得到启发
  • 四、Java 内存模型
  • 五、原子性
    • 5.1、什么是原子性
    • 5.2、如何保证原子性
  • 六、可见性
    • 6.1、什么是可见性
    • 6.2、如何保证可见性
  • 七、有序性
    • 7.1、什么是有序性
    • 7.2、如何保证有序性

一、为什么要学习并发编程

对于 “我们为什么要学习并发编程?” 这个问题,就好比 “我们为什么要学习政治?” 一样,我们(至少作为学生党是这样)平常很少接触到,然后背了一堆 “正确且伟大无比的废话”,最终沦为八股被快速遗忘。

直到我开始去深入了解这块知识而不是盲目背诵的时候,我才明白,它正确且伟大无比,但不是废话。

尽管并发编程的各种底层原理以及其庞大的知识体系容易让人心生畏惧,但是 Java 语言和 Java 虚拟机都提供了相当多的并发工具,替我们隐藏了很多的线程并发细节,使得我们在编码时能更关注业务逻辑,把并发编程的门槛降低了不少。

但是无论语言、中间件和框架再如何先进,我们都不应该完全依赖于它们完成并发处理的所有事情,了解并发的内幕并学习其中的思想,仍然是成为一个高级程序员的必经之路。

我想,上面这段话大概可以回答 “我们为什么要学习并发编程?” 这个问题了。

二、为什么需要并发编程

不知道各位有没有听说过被誉为计算机第一定律的摩尔定律,它是英特尔创始人之一戈登 · 摩尔长期观察总结出来的经验,虽然不是严格推导出来的真理,但最起码迄今为止仍然是令人深信不疑的。其核心内容通俗来说就是 处理器的性能每隔两年就会翻一倍。看起来像个废话。

而事实上,当今多核 CPU 的发展速度也确实正在支撑着摩尔定律的有效性。在时代的大背景下,并发编程已成燎原之势,通过并发编程的形式将多核 CPU 的计算能力发挥到极致,性能得到提升。

举个例子,在当今诸神黄昏的图像处理领域,很多图像处理算法,在代码初步编写完毕并调试正确后,其实仍然需要进行一个漫长的优化过程。因为尽管有些算法的处理效果很棒,但是如果运算太过耗时,还是无法集成进产品给用户使用的。

对于一副 1000 x 800 分辨率的图像,我们最原始的处理思路就是从第 1 个像素开始,一直遍历计算到最后一个像素。那么面对如此庞大且复杂的计算量,为了提高算法的性能,最直接也最容易实现的想法就是基于多线程充分利用多核 CPU 的计算能力。

可以将整个图像分成若干块,比如我们的 CPU 是 8 核的,那么可以分成 8 块,每块图像大小为 1000 * 100 像素,我们可以创建 8 个线程,每个线程处理一个图像块,每个 CPU 分配执行一个线程。这样,运算速度将得到明显的提升。

当然了,这样操作后,运算速度并不会恐怖的提升 4 倍,因为线程创建和释放以及上下文切换都有一定的损耗。

这里摘录《Java 并发编程的艺术》书中的一段话来回答这个问题,我们为什么需要并发线程?

多核 CPU 时代的到来打破了单核 CPU 对多线程效能的限制。多个 CPU 意味着每个线程可以使用自己的 CPU 运行,这减少了线程上下文切换的开销,但随着对应用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切的需求。

而至于多核 CPU 盛行的原因,《深入理解 Java 虚拟机 - 第 3 版》一书中也有所涉及,这里我略作修改摘录如下:

多任务处理在现代计算机操作系统中几乎已是一项必备的功能了。在许多场景下,让计算机同时去做几件事情,不仅是因为计算机的运算能力强大了,更重要的原因是计算机的运算速度与它的存储和通信子系统速度的差距太大,这样 CPU 不得不花费大量的时间等待其他资源,比如磁盘 I/O、网络通信或者数据库访问等。

为此,我们就必须使用一些手段去把处理器的运算能力“压榨”出来,否则就会造成很大的性能浪费,而让计算机同时处理几项任务则是最容易想到,也被证明是非常有效的“压榨”手段。

另外,除了充分利用计算机处理器的能力外,一个服务端要同时对多个客户端提供服务,则是另一个更具体的并发应用场景。

三、从物理机中得到启发

事实上,物理机遇到的并发问题与虚拟机中的情况有很多相似之处,物理机对并发的处理方案对虚拟机的实现也有相当大的参考意义,因此,我们有必要学习下物理机中处理问题的方法。

上文说过可以使用并发编程来充分利用 CPU 的资源,其中一个主要原因就是计算机的存储设备与 CPU 的运算速度有着几个数量级的差距,这样 CPU 不得不花费大量的时间去等待其他资源。

这是软件层面,而在硬件层面上,现代计算机系统都会在内存与 CPU 之间加入一层或多层读写速度尽可能接近 CPU 运算速度的高速缓存来作为缓冲。

将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

为此,这不可避免的带来了一个新的问题:缓存一致性(Cache Coherence)。

就是说当多个 CPU 的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。如果真的发生这种情况,那同步回到主内存时该以谁的缓存数据为准呢?

为了解决一致性的问题,需要各个 CPU 访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。于是,我们引出了内存模型的概念。

在物理机层面,内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。

显然,不同架构的物理机器可以拥有不一样的内存模型,而 Java 虚拟机也拥有自己的内存模型,称为 Java 内存模型(Java Memory Model,JMM),其目的就是为了屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

当然了,JMM 与这里我们介绍的物理机的内存模型具有高度的可类比性。

四、Java 内存模型

JMM 规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory)。

线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。

此处的主内存可以与前面所说的物理机的主内存类比,当然,实际上它仅是虚拟机内存的一部分,工作内存可与前面讲的高速缓存类比。

《Java 并发编程的艺术》中把 “工作内存” 称为 “本地内存”(Local Memory)。 “工作内存” 是《深入理解 Java 虚拟机 - 第 3 版》这本书中的写法。

多提一嘴,这里的变量其实和我们日常编程中所说的变量不一样,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后面这俩是线程私有的,不会被共享,自然就不会存在竞争问题。各位知道就好,不必太过深究。

五、原子性

5.1、什么是原子性

类比物理机,拥有缓存一致性协议来规定主内存和高速缓存之间的操作逻辑,那么 JMM 中主内存与工作内存之间有没有具体的交互协议呢?

Of Course!JMM 中定义了以下 8 种操作规范来完成一个变量从主内存拷贝到工作内存、以及从工作内存同步回主内存这一类的实现细节。Java 虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。

暂时放下到底是哪 8 种操作,我们先谈何为原子?

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

举个经典的简单例子,银行转账,A 像 B 转账 100 元。转账这个操作其实包含两个离散的步骤:

  • 步骤 1:A 账户减去 100
  • 步骤 2:B 账户增加 100

我们要求转账这个操作是原子性的,也就是说步骤 1 和步骤 2 是顺续执行且不可被打断的,要么全部执行成功、要么执行失败。

试想一下,如果转账操作不具备原子性会导致什么问题呢?

比如说步骤 1 执行成功了,但是步骤 2 没有执行或者执行失败,就会导致 A 账户少了 100 但是 B 账户并没有相应的多出 100。

对于上述这种情况,符合原子性的转账操作应该是如果步骤 2 执行失败,那么整个转账操作就会失败,步骤 1 就会回滚,并不会将 A 账户减少 100。

OK,了解了原子性的概念后,我们再来看 JMM 定义的 8 种原子操作具体是啥,以下了解即可,没必要死记:

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量

事实上,对于 doublelong 类型的变量来说,load、store、read 和 write 操作在某些平台上允许有例外,称为 “long 和 double 的非原子性协定”,不过一般不需要我们特别注意,这里就不再过多赘述了。

这 8 种操作当然不是可以随便用的,为了保证 Java 程序中的内存访问操作在并发下仍然是线程安全的,JMM 规定了在执行上述 8 种基本操作时必须满足的一系列规则。

这我就不一一列举了,多提这么一嘴的原因就是下文会涉及一些这其中的规则,为了防止大家看的时候云里雾里,所以先前说明白比较好。

上面我们举了一个转账的例子,那么,在具体的代码中,非原子性操作可能会导致什么问题呢?

看下面这段代码,各位不妨考虑一个的问题,如果两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果一定是 0 吗?

耳熟能详的问题,我们无法保证这段代码执行结果的一定性(正确性),可能是正数、也可能是负数、当然也可能是 0。

那么,我们就把这段代码称为线程不安全的,就是说在单线程环境下正常运行的一段代码,在多线程环境中可能发生各种意外情况,导致无法得到正确的结果。

从线程安全的角度来反向理解线程不安全的概念可能更容易点,这里参考《Java 并发编程实践》上面的一句话:

一段代码在被多个线程访问后,它仍然能够进行正确的行为,那这段代码就是线程安全的。

至于这段代码线程不安全的原因,就是 Java 中对静态变量自增和自减操作并不是原子操作,它俩其实都包含三个离散的操作:

  • 步骤 1:读取当前 i 的值
  • 步骤 2:将 i 的值加 1(减 1)
  • 步骤 3:写回新值

可以看出来这是一个 读 - 改 - 写 的操作。

i ++ 操作为例,我们来看看它对应的字节码指令:

上方这段代码对应的字节码是这样的:

简单解释下这些字节码指令的含义:

  • getstatic i:获取静态变量 i 的值
  • iconst_1:准备常量 1
  • iadd:自增(自减操作对应 isub)
  • putstatic i:将修改后的值存入静态变量 i

如果是在单线程的环境下,先自增 5000 次,然后再自减 5000 次,那当然不会发生任何问题。

但是在多线程的环境下,由于 CPU 时间片调度的原因,可能 Thread1 正在执行自增操作着呢,CPU 剥夺了它的资源占用,转而分配给了 Thread2,也就是发生了线程上下文切换。这样,就可能导致本该是一个连续的读改写动作(连续执行的三个步骤)被打断了。

下图出现的就是结果最终是负数的情况:

总结来说,如果多个 CPU 同时对某个共享变量进行读-改-写操作,那么这个共享变量就会被多个 CPU 同时处理,由于 CPU 时间片调度等原因,某个线程的读-改-写操作可能会被其他线程打断,导致操作完后共享变量的值和我们期望的不一致。

另外,多说一嘴,除了自增自减,我们常见的 i = j 这个操作也是非原子性的,它分为两个离散的步骤:

  • 步骤 1:读取 j 的值
  • 步骤 2:将 j 的值赋给 i

5.2、如何保证原子性

那么,如何实现原子操作,也就是如何保证原子性呢?

对于这个问题,其实在处理器和 Java 编程语言层面,它们都提供了一些有效的措施,比如处理器提供了总线锁和缓存锁,Java 提供了锁和循环 CAS 的方式,这里我们简单解释下 Java 保证原子性的措施。

由 Java 内存模型来直接保证的原子性变量操作包括 readloadassignusestorewrite 这 6 个,我们大致可以认为,基本数据类型的访问、读写都是具备原子性的(例外就是 long 和 double 的非原子性协定,各位只要知道这件事情就可以了,无须太过在意这些几乎不会发生的例外情况)。

如果应用场景需要一个更大范围的原子性保证,Java 内存模型还提供了 lockunlock 操作来满足这种需求。

尽管 JVM 并没有把 lockunlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorentermonitorexit 来隐式地使用这两个操作。这两个字节码指令反映到 Java 代码中就是同步块 — synchronized 关键字,因此在 synchronized 块之间的操作也具备原子性。

而除了 synchronized 关键字这种 Java 语言层面的锁,juc 并发包中的 java.util.concurrent.locks.Lock 接口也提供了一些类库层面的锁,比如 ReentrantLock

另外,随着硬件指令集的发展,在 JDK 5 之后,Java 类库中开始使用基于 cmpxchg 指令的 CAS 操作(又来一个重点),该操作由 sun.misc.Unsafe 类里面的 compareAndSwapInt()compareAndSwapLong() 等几个方法包装提供。不过在 JDK 9 之前 Unsafe 类是不开放给用户使用的,只有 Java 类库可以使用,譬如 juc 包里面的整数原子类,其中的 compareAndSet()getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作来实现。

使用这种 CAS 措施的代码也常被称为无锁编程(Lock-Free)。

六、可见性

6.1、什么是可见性

回到物理机,前文说过,由于引入了高速缓存,不可避免的带来了一个新的问题:缓存一致性。而同样的,这个问题在 Java 虚拟机中同样存在,表现为工作内存与主内存的同步延迟,也就是内存可见性问题。

何为可见性?就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。

回顾下 Java 内存模型:

从上图来看,如果线程 A 与线程 B 之间要通信的话,必须要经历下面 2 个步骤:

  • 1)线程 A 把工作内存 A 中更新过的共享变量刷新到主内存中去
  • 2)线程 B 到主内存中去读取线程 A 之前已更新过的共享变量

也就是说,线程 A 在向线程 B 的通信过程必须要经过主内存。

那么,这就可能出现一个问题,举个简单的例子,看下面这段代码:

// 线程 1 执行的代码
int i = 0;
i = 1;
// 线程 2 执行的代码
j = i;

当线程 1 执行 i = 1 这句时,会先去主内存中读取 i 的初始值,然后加载到线程 1 的的工作内存中,再赋值为1,至此,线程 1 的工作内存当中 i 的值变为 1 了,不过还没有写入到主内存当中。

如果在线程 1 准备把新的 i 值写回主内存的时候,线程 2 执行了 j = i 这条语句,它会去主存读取 i 的值并加载到线程 2 的工作内存当中,而此时主内存当中 i 的值还是 0,那么就会使得 j 的值为 0,而不是 1。

这就是内存可见性问题,线程 1 修改了共享变量 i 的值,线程 2 并没有立即得知这个修改。

6.2、如何保证可见性

各位可能脱口而出使用 volatile 关键字修饰共享变量,但除了这个,容易被大家忽略的是,其实 sunchronizedfinal 这俩关键字也能保证可见性。

上面我提过一嘴,为了保证 Java 程序中的内存访问操作在并发下仍然是线程安全的,JMM 规定了在执行 8 种基本原子操作时必须满足的一系列规则,这其中有一条规则正是 sychronized 能够保证原子性的理论支撑,如下:

  • 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)

也就是说 synchronized在修改了工作内存中的变量后,解锁前会将工作内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。

至于 final 关键字的可见性需要结合其内存语义深入来讲,这里就先简单的概括下:被 final 修饰的字段在构造器中一旦被初始化完成,并且构造器没有把 this 的引用传递出去,那么在其他线程中就能看见 final 字段的值。

七、有序性

7.1、什么是有序性

OK,说完了可见性,我们再回到物理机,其实除了增加高速缓存之外,为了使 CPU 内部的运算单元能尽量被充分利用,CPU 可能会对输入代码进行乱序执行优化,CPU 会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。

与之类似的,Java 的编译器也有这样的一种优化手段:指令重排序(Instruction Reorder)。

那么,既然能够优化性能,重排序可以没有限制的被使用吗?

当然不,在重排序的时候,CPU 和编译器都需要遵守一个规矩,这个规矩就是 as-if-serial 语义:不管怎么重排序,单线程环境下程序的执行结果不能被改变。

为了遵守 as-if-serial 语义,CPU 和编译器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。

那么这里,我们又引出了 “数据依赖性” 的概念。

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。

数据依赖性分为三种类型:写后读、写后写、读后写,看下图

上面 3 种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

其实考虑数据依赖关系的时候,各位可以通过画图来直观的判断。举个例子:

int a = 1;		 // A
int b = 2;		 // B
int sum = a + b; // C

上面 3 个操作的数据依赖关系如下图所示:

可以看出,A 和 C、B 和 C 之间存在数据依赖关系,因此在最终执行的指令序列中,C 不能被重排序到 A 或B 的前面。但 A 和 B 之间没有数据依赖关系,所以 CPU 和处理器可以重排序 A 和 B 之间的执行顺序。如下是程序的两种执行顺序:

看起来好像没啥问题,重排序之后程序的结果并没有发生改变,还提升了性能。

然而,很不幸的是,我们这里所说的数据依赖性仅针对单个 CPU 中执行的指令序列和单个线程中执行的操作,不同 CPU 之间和不同线程之间的数据依赖性是不被 CPU 和编译器考虑的。

这就是为啥我在写 as-if-serial 语义的时候把 “单线程” 加粗的目的了。

看下面这段代码:

假设有两个线程 A 和 B,A 首先执行 writer() 方法,随后 B 线程接着执行 reader() 方法。线程 B 在执行操作 4 时,能否看到线程 A 在操作 1 把共享变量 a 修改成了 1 呢?

答案是不一定。

由于操作 1 和操作 2 没有数据依赖关系,CPU 和编译器可以对这两个操作重排序;同样的,操作 3 和操作 4 没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。

以操作 1 和操作 2 重排序为例,可能会产生什么效果呢?

如上图右边所示,程序执行时,线程 A 首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还没有被线程 A 写入,因此线程 B 读到的 a 值仍然是 0。也就是说在这里多线程程序的语义被重排序破坏了。

这样,我们可以得出结论:CPU 和 Java 编译器为了优化程序性能,会自发地对指令序列进行重新排序。在多线程的环境下,由于重排序的存在,就可能导致程序运行结果出现错误。

了解了重排序的概念,我们可以这样总结下 Java 程序天然的有序性:

  • 如果在本线程内观察,所有的操作都是有序的(简单来说就是线程内表现为串行)
  • 如果在一个线程中观察另一个线程,所有的操作都是无序的(这个无序主要就是指 “指令重排序” 现象和 “工作内存与主内存同步延迟” 现象)

7.2、如何保证有序性

Java 语言提供了 volatilesynchronized 两个关键字来保证线程之间操作的有序性。

volatile 本身除了保证可见性的语义外,还包含了禁止指令重排序的语义,所以天生就具有保证有序性的功能。

synchronized 保证有序性的理论支撑,仍然是 JMM 规定在执行 8 种基本原子操作时必须满足的一系列规则中的某一个提供的:

  • 一个变量在同一个时刻只允许一条线程对其进行 lock 操作

这个规则决定了持有同一个锁的两个 synchronized 同步块只能串行地进入。

不是很难理解吧,通俗来说,synchronized 通过排他锁的方式保证了同一时间内,被 synchronized 修饰的代码是单线程执行的。所以,这就满足了 as-if-serial 语义的一个关键前提,那就是单线程,这样,有了 as-if-serial 语义的保证,单线程的有序性也就得到保障了。

Happens-before 原则:

Happens-before 是 JMM 的灵魂,它是判断数据是否存在竞争,线程是否安全的非常有用的手段。为了知识体系的完整性,这里简单提一下,后续文章会详细解释的。

如果 Java 内存模型中所有的有序性都仅靠 volatile 和 synchronized 来完成,那么有很多操作都将会变得非常啰嗦,但是我们在编写 Java 并发代码的时候并没有察觉到这一点,这就归功于 “先行发生”(Happens-Before)原则。

依赖这个原则,我们可以通过几条简单规则快速解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入 Java 内存模型苦涩难懂的定义之中。

以上就是详细分析Java内存模型的详细内容,更多关于Java内存模型的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java 高并发三:Java内存模型和线程安全详解

    网上很多资料在描述Java内存模型的时候,都会介绍有一个主存,然后每个工作线程有自己的工作内存.数据在主存中会有一份,在工作内存中也有一份.工作内存和主存之间会有各种原子操作去进行同步. 下图来源于这篇Blog 但是由于Java版本的不断演变,内存模型也进行了改变.本文只讲述Java内存模型的一些特性,无论是新的内存模型还是旧的内存模型,在明白了这些特性以后,看起来也会更加清晰. 1. 原子性 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰

  • 在Java内存模型中测试并发程序代码

    让我们来看看这段代码: import java.util.BitSet; import java.util.concurrent.CountDownLatch; public class AnExample { public static void main(String[] args) throws Exception { BitSet bs = new BitSet(); CountDownLatch latch = new CountDownLatch(1); Thread t1 = ne

  • Java内存模型知识汇总

    为什么要有内存模型 在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机内存模型的基础上做了哪些事情.要说计算机的内存模型,就要说一下一段古老的历史,看一下为什么要有内存模型. 内存模型,英文名Memory Model,他是一个很老的老古董了.他是与计算机硬件有关的一个概念.那么我先给你介绍下他和硬件到底有啥关系. CPU和缓存一致性 我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道.而计算机

  • 浅析Java内存模型与垃圾回收

    1.Java内存模型 Java虚拟机在执行程序时把它管理的内存分为若干数据区域,这些数据区域分布情况如下图所示: 程序计数器:一块较小内存区域,指向当前所执行的字节码.如果线程正在执行一个Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计算器值为空. Java虚拟机栈:线程私有的,其生命周期和线程一致,每个方法执行时都会创建一个栈帧用于存储局部变量表.操作数栈.动态链接.方法出口等信息. 本地方法栈:与虚拟机栈功能类似,只不过虚拟机栈为虚拟机执行J

  • 浅谈Java并发中的内存模型

    什么是JavaMemoryModel(JMM)? JMM通过构建一个统一的内存模型来屏蔽掉不同硬件平台和不同操作系统之间的差异,让Java开发者无需关注不同平台之间的差异,达到一次编译,随处运行的目的,这也正是Java的设计目的之一. CPU和内存 在讲JMM之前,我想先和大家聊聊硬件层面的东西.大家应该都知道执行运算操作的CPU本身是不具备存储能力的,它只负责根据指令对传递进来的数据做相应的运算,而数据存储这一任务则交给内存去完成.虽然内存的运行速度虽然比起硬盘快非常多,但是和3GHZ,4GH

  • Java内存模型JMM详解

    Java Memory Model简称JMM, 是一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性.是否可以重排序等问题的无关具体平台的统一的保证.(可能在术语上与Java运行时内存分布有歧义,后者指堆.方法区.线程栈等内存区域). 并发编程有多种风格,除了CSP(通信顺序进程).Actor等模型外,大家最熟悉的应该是基于线程和锁的共享内存模型了.在多线程编程中,需要注意三类并发问题: ·原子性 ·可见性 ·重排序 原子性涉及到,一个线程执行一个复合操作的时候,其他线程是否能够看

  • Java8内存模型PermGen Metaspace实例解析

    一.JVM 内存模型 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 1.虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建.栈里面存着的是一种叫"栈帧"的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用).操作数栈.方法出口等信息.栈的大小可以固定也可以动态扩展.当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值,我们通过下面这段程序可以测

  • Java内存区域和内存模型讲解

    一.Java内存区域 方法区(公有):用户存储已被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码等数据.异常状态 OutOfMemoryError. 堆(公有):是JVM所管理的内存中最大的一块.唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配.Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为"GC堆".异常状态 OutOfMemoryError. 虚拟机栈(线程私有): 描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,用户存储局部变

  • 学习Java内存模型JMM心得

    有时候编译器.处理器的优化会导致runtime与我们设想的不一样,为此Java对编译器和处理器做了一些限制,JAVA内存模型(JMM)将这些抽象出来,这样编写代码时就无需考虑那么多底层细节,并保证"只要遵循JMM的规则编写程序,其运行结果一定是正确的". JMM的抽象结构 在Java中,所有的实例.静态变量存储在堆内存中,堆内存是可以在线程间共享的,这部分也称为共享变量.而局部变量.方法定义参数.异常处理参数是在栈中的,栈内存不在线程间共享. 而由于编译器.处理器的优化,会导致共享变量

  • 详细分析Java内存模型

    目录 一.为什么要学习并发编程 二.为什么需要并发编程 三.从物理机中得到启发 四.Java 内存模型 五.原子性 5.1.什么是原子性 5.2.如何保证原子性 六.可见性 6.1.什么是可见性 6.2.如何保证可见性 七.有序性 7.1.什么是有序性 7.2.如何保证有序性 一.为什么要学习并发编程 对于 "我们为什么要学习并发编程?" 这个问题,就好比 "我们为什么要学习政治?" 一样,我们(至少作为学生党是这样)平常很少接触到,然后背了一堆 "正确且

  • java内存模型jvm虚拟机简要分析

    目录 主内存和工作内存 内存间的交互操作 原子性.可见性.有序性 原子性 可见性 有序性 主内存和工作内存 Java 内存模型规定了所有的变量都存储在主内存中, 每条线程有自己的工作内存 线程的工作内存中保存了被该线程使用的变量的主内存副本, 线程对变量的所有操作 (读取.赋值等) 都必须在工作内存中进行, 而不能直接读写主内存中的数据 不同的线程之间也无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要通过主内存来完成 内存间的交互操作 原子性.可见性.有序性 Java 内存模型是围绕

  • 详细分析java并发之volatile关键字

    Java面试中经常会涉及关于volatile的问题.本文梳理下volatile关键知识点. volatile字意为"易失性",在Java中用做修饰对象变量.它不是Java特有,在C,C++,C#等编程语言也存在,只是在其它编程语言中使用有所差异,但总体语义一致.比如使用volatile 能阻止编译器对变量的读写优化.简单说,如果一个变量被修饰为volatile,相当于告诉系统说我容易变化,编译器你不要随便优化(重排序,缓存)我. Happens-before 规范上,Java内存模型遵

  • Java内存模型之happens-before概念详解

    简介 happens-before是JMM的核心概念.理解happens-before是了解JMM的关键. 1.设计意图 JMM的设计需要考虑两个方面,分别是程序员角度和编译器.处理器角度: 程序员角度,希望内存模型易于理解.易于编程.希望是一个强内存模型. 编译器和处理器角度,希望减少对它们的束缚,以至于编译器和处理器可以做更多的性能优化.希望是一个弱内存模型. ​因此JSR-133专家组设计JMM的核心目标就两个: 为程序员提供足够强的内存模型对编译器和处理器的限制尽可能少 ​下面通过一段代

  • 深入了解volatile和Java内存模型

    目录 前言 为什么我们需要volatile? 保证数据的可见性 禁止指令重排序 Java内存模型(JMM) JMM下的内存逻辑结构 内存交互的操作 重排序 Volatile实现原理 禁止重排序实现原理 可见性实现原理 深入内存屏障——Store Buffer和Invalid Queue MESI协议 总结 前言 在本篇文章当中,主要给大家深入介绍Volatile关键字和Java内存模型.在文章当中首先先介绍volatile的作用和Java内存模型,然后层层递进介绍实现这些的具体原理.JVM底层是

  • Java内存模型原子性原理及实例解析

    这篇文章主要介绍了Java内存模型原子性原理及实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本文就具体来讲讲JMM是如何保证共享变量访问的原子性的. 原子性问题 原子性是指:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行. 下面就是一段会出现原子性问题的代码: public class AtomicProblem { private static Logger logger = LoggerFactory.

  • Java内存模型可见性问题相关解析

    这篇文章主要介绍了Java内存模型可见性问题相关解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言 之前的文章中讲到,JMM是内存模型规范在Java语言中的体现.JMM保证了在多核CPU多线程编程环境下,对共享变量读写的原子性.可见性和有序性. 本文就具体来讲讲JMM是如何保证共享变量访问的可见性的. 什么是可见性问题 我们从一段简单的代码来看看到底什么是可见性问题. public class VolatileDemo { boolean

随机推荐