从JVM设计角度解读Java内存模型

要确保每个处理器在任意时刻都知道其他处理器在进行的工作,这将开销巨大。多数情况下,这完全没必要,可随意放宽存储一致性,换取性能的提升。

在架构定义的内存模型中将告诉应用程序可以从内存系统中获取怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证。为了使 Java 开发人员无须关心不同架构上内存模型之间的差异, Java 还提供了自己的内存模型JMM ,并且 JVM 通过在适当的位置上插入内存栅栏来屏蔽 JMM 与底层平台内存模型之间的差异。

程序执行一种简单的假设:想象在程序中之存在唯一的操作执行顺序,而不考虑这些操作在何种处理器上执行,并且在每次读取变量时,都能获得在执行序列中最近一次写入该变量的值。这种乐观的模型被称为串行一致性。软件开发人员经常会错误地假设存在串行一致性。但是在任何一款现代多处理器架构中都不会提供这种串行一致性, JMM 也是如此。冯诺依曼模型这种经典的穿行计算模型,只能近似描述现代多处理器的行为。

在现在支持共享内存的多处理和编译器中,当跨线程共享数据时,会出现一些奇怪的情况,除非通过使用内存栅栏来防止这种情况的发生。幸运的是, Java 程序不需要制定内存栅栏的位置,只需要通过正确地使用同步就可以。

1.2 重排序

程序清单16-1 如果没有包含足够的同步,将产生奇怪的结果

public class ReorderingDemo {

static int x = 0, y = 0;

static int a = 0, b = 0;

public static void main(String[] args) throws Exception {

x = y = a = b = 0;

Thread one = new Thread() {

public void run() {

a = 1;

x = b;

}

};

Thread two = new Thread() {

public void run() {

b = 1;

y = a;

}

};

one.start();

two.start();

one.join();

two.join();

System.out.println(x + ", " + y);

}

程序清单 16-1 ReorderingDemo 说明了在没有正确的同步情况下,即使要推断最简单的并发程序的行为也很难。图 16-1 给出了一种可能由于不同执行顺序而输出的结果。

这种各种使操作延迟或者看似混乱执行的不同原因,都可以归为重排序。

ReorderingDemo 很简单,但是要列举出他所有可能的结果却非常困难。 内存级别的重排序会使程序的行为不可预测 。如果没有同步,那么推断出执行顺序将是非常困难的,而要确保在程序中正确地使用同步却是非常容易的。 同步将限制编译器、运行时和硬件对内存操作的重排序的方式,从而在实施重排序时不会破坏 JMM 提供的可见性保证。

注:在大多数主流的处理器架构中,内存模型都非常强大,使得读取 volatile 变量的性能与读取非 volatile 变量的性能大致相当。

1.3 Java 内存模型简介

JMM 是通过各种操作来定义,包括对变量的读写操作,监视器 monitor 的加锁和释放操作,以及线程的启动和合并操作, JMM 为程序中所有的操作定义了一个偏序关系,称为 Happens-before ,要想保证执行操作 B 的线程看到 A 的结果(无论 A 和 B 是否在同一个线程中执行),那么 A和 B 之间必须满足 Happens-before 关系。如果没有这个关系,那么 JVM 可以对他们任意的重排序。

当一个变量被多个线程读取并且至少被一个线程写入时,如果在读操作和写操作之间没有依照Happens-before 来排序,那么就会产生数据竞争的问题。在正确使用同步的程序中不存在数据竞争,并会表现出串行一致性,这意味着程序中的所有操作都会按照一种固定的和全局的顺序执行。

图 16-2 给出了当两个线程使用同一个锁进行同步时,在他们之间的 Happens-before 关系。在线程 A 内部的所有操作都按照他们在源程序中的先后顺序来排序,在线程 B 内部的操作也是如此。由于 A 释放了锁 M ,并且 B 随后获得了锁 M ,因此 A 中所有在释放锁之前的操作,也就位于 B 中请求锁之后的所有操作之前。如果这两个线程是在不同的锁上进行同步的,那么就不能推断他们之间的动作顺序,因为他们之间不存在 Happens-before 关系。

1.4 借助同步

由于 Happens-Before 的排序功能很强大,因此有时候可以 ” 借助( Piggyback ) ” 现有同步机制的可见性属性。这需要将 Happens-Before 的程序规则与其他某个顺序规则(通常是监视器锁规则或者 volatile 变量规则)结合起来,从而对某个未被锁保护的变量的访问操作进行排序。这项技术由于对语句的顺序非常敏感,因此很容易出错。他是一项高级技术,并且只有当需要最大限度地提升某些类(例如 ReentrantLock )的性能时,才应该使用这项技术。同时,因为在使用中很容易出错,因此也要谨慎使用。

在 FutureTask 的保护方法 AbstractQueuedSynchronizer 中说明了如何使用这种 “ 借助 ” 技术。

AQS 维护了一个标识同步器状态的整数, FutureTask 用这个整数来保存任务的状态:正在运行、已完成和已取消。但 FutureTask 还维护了其他一些变量,例如计算的结果。当一个线程调用set 方来保存结果并且另一线程调用 get 来获取该结果时,这两个线程最好按照 Happens-Before进行排序。这可以通过将执行结果的引用声明为 volatile 类型来实现,但利用现在的同步机制可以更容易地实现相同的功能。

程序清单16-2 说明如何借助同步的FutureTask的内部类

FutureTask 在设计时能够确保,在调用 tryAccquireShared 之前总能成功调用 tryReleaseShard。 tryReleaseShard 会写入一个 volatile 类型的变量,而 tryAccquireShard 将读取这个变量。程序清单 16-2 给出了 innerGet 和 innerSet 等方法,在保存和获取 result 时将调用这些方法。由于 innerSet 将在调用 releaseShared (这又将调用 tryReleaseShard )之前写入 result ,并且 innerGet 将在调用 acquireShared (这又将调用 tryAccquireShared )之后读取 result ,因此将程序顺讯规则与 volatile 变量规则结合在一起,就可以确保 innerSet 中的写入操作在 innerGer 之前之前。

之所以将这项技术称为 “ 借助 ” ,是因为它使用了一种现有的 Happens- Before 顺序来确保对象 X 的可见性,而不是专门为了发布 X 而创建一种 Happens-Before 顺序。在类库中提供的其他Happens-Before 排序包括:

  • 将一个元素放入一个线程安全容器的操作将在另一个线程从该容器中获得这个元素的操作之前执行

  • 在 CountDownLatch 上的倒数操作将在线程从闭锁上的 await 方法返回之前执行

  • 释放 Semaphore 许可的操作将在从该 Semaphore 上获得一个许可之前执行

  • Future 表示的任务的所有操作将在从 Future.get 中返回之前执行

  • 向 Executor 提交一个 Runnable 或 Callable 的操作将在任务开始执行之前执行

  • 一个线程到达 CyclicBarrier 或 Exchange 的操作将在其他到达该栅栏或交换点的线程被释放之前执行。如果 CyclicBarrier 使用一个栅栏操作,那么到达栅栏的操作将在栅栏操作之前执行,而栅栏操作又会在线程从栅栏中释放之前执行。

2 、发布


第三章介绍了如何安全的或者不正确的发布一个对象,其中介绍的各种技术都依赖 JMM 的保证,而造成发布不正确的原因就是在 “ 发布一个共享对象 ” 与 “ 另外一个线程访问该对象 ” 之间缺少一种 happens-before 关系。

2.1  不安全的发布

当缺少 happens-before 关系时,就可能会发生重排序,这就解释了为什么在没有充分同步的情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象。假入初始化一个对象时需要写入多个变量(多个域),在发布该对象时,则可能出现如下情况,导致发布了一个被部分构造的对象:

init field a

init field b

发布ref

init field c

错误的延迟初始化将导致不正确的发布,如下程序清单 16-3 。

注:除了不可变对象以外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行

程序清单16-3 不安全的延迟初始化

public class UnsafeLazyInitialization {

private static Object resource;

public static Object getInstance(){

if (resource == null){

resource = new Object(); //不安全的发布

}

return resource;

}

}

2.2  安全发布

借助于类库中现在的同步容器、使用锁保护共享变量、或都使用共享的 volatile 类型变量,都可以保证对该变量的读取和写入是按照 happens-before 关系来排序。

注: happens-before 事实上可以比安全发布承诺更强的可见性与排序性

2.3  安全初始化模式

方式一:加锁保证可见性与排序性

getInstance 的代码路径很短,只包括一个判断预见和一个预测分支,因此如果在没有被多个线程频繁调用或者在不会出现激烈竞争的情况下,可以提供较为满意的性能。

程序清单16-4 线程安全的延迟初始化

public class SafeLazyInitialization {

private static Object resource;

public synchronized static Object getInstance(){

if (resource == null){

resource = new Object();

}

return resource;

}

}

方式二:提前初始化

在初始化器中采用了特殊的方式来处理静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化是由 JVM 在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于 JVM 将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。

因此,无论是在被构造期间还是被引用时,静态初始化的对象都不需要显示的同步。

程序清单16-5 提前初始化

public class EagerInitialization {

private static Object resource = new Object();

public static Object getInstance(){

return resource;

}

}

方式三:延迟初始化展位模式,建议

通过静态初始化和 JVM 的延迟加载机制结合起来可以形成一种延迟初始化的技术,从而在常见的代码路径中不需要同步。

程序清单16-6 掩藏初始化占位类模式

public class ResourceFactory {

private static class ResourceHolder{

public static Object resource = new Object();

}

public static Object getInstance(){

return ResourceHolder.resource;

}

}

方式四: DCL 双重加锁机制,注意保证 volatile 类型,否则出现一致性问题( jdk5.0+ )

DCL 实际是一种糟糕的方式,是一种 anti-pattern ,它只在 JAVA1.4 时代好用,因为早期同步的性能开销较大,用来避免不必要的开销或者降低程序的启动时间,但是目前 DCL 已经被广泛的废弃不用,因为促使该模式出现的驱动力已经不在(无竞争同步的执行速度很慢,以及 jvm 启动时很慢),他不是一个高效的优化措施。

程序清单16-7 双重加锁

public class DoubleCheckedLocking {

private static volatile Object resource;

public static Object getInstance(){

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

以上是字节二面的一些问题,面完之后其实挺后悔的,没有提前把各个知识点都复习到位。现在重新好好复习手上的面试大全资料(含JAVA、MySQL、算法、Redis、JVM、架构、中间件、RabbitMQ、设计模式、Spring等),现在起闭关修炼半个月,争取早日上岸!!!

下面给大家分享下我的面试大全资料

  • 第一份是我的后端JAVA面试大全

image.png

后端JAVA面试大全

  • 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理

字节二面拜倒在“数据库”脚下,闭关修炼半个月,我还有机会吗?

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

  • 第三份是Spring全家桶资料

字节二面拜倒在“数据库”脚下,闭关修炼半个月,我还有机会吗?

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
第一份是我的后端JAVA面试大全

[外链图片转存中…(img-IbSyvFUB-1713473541707)]

后端JAVA面试大全

  • 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理

[外链图片转存中…(img-uqKG2J4X-1713473541708)]

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理

  • 第三份是Spring全家桶资料

[外链图片转存中…(img-lmWgiluJ-1713473541708)]

MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值