JMM如何解决重排序问题

随着现代CPU从提升单核性能转变到提升多核性能,并发编程显得愈发重要。通过多线程的协同工作使得程序的执行效率更高!

同时,计算机软硬件为了提升程序执行效率,对指令做了重排序操作,分为三种类型:

  1. 编译器优化的重排序:编译器在不改变单线程程序的前提下进行指令的重排序;
  2. 指令级并行重排序:现代CPU采用了指令级并行技术,包括流水线技术以及针对指令顺序进行重排序的方式;
  3. 内存系统的重排序:由于处理器使用了读/写缓冲区,使得加载和存储操作看起来是乱序执行的。

在这里插入图片描述

Java内存模型JMM是一个抽象概念,涵盖了缓存、写缓冲区、寄存器以及其它硬件和编译器优化,用于解决Java多线程对共享数据的读写一致性问题。在JMM中变量的值从线程A到线程B要经过两个阶段:线程A将工作内存中更新过的共享变量刷到主内存中;线程B从主内存中读取线程A更新过的变量。

基于上述软硬件层面的重排序操作以及JMM内存模型共享数据的读写方式,就有可能带来数据同步问题。也就是说写变量的位置不一定早于读变量位置。

对于理想化的顺序一致内存模型来说,其具备两个特性:

  1. 一个线程中的所有操作必须按照程序的顺序来执行(无各种指令重排序);
  2. (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序(每个操作都必须原子执行且立刻对所有线程可见,大概就是全是volatile变量)

因此,我们可以说,在理想的一致性内存模型中,是不存在数据同步问题的,但是存在性能问题。所有操作被串行化了,没有使用到CPU的多核特性。

JMM为了保证确保对CPU多核特性的充分利用,提高程序执行的效率,针对未正确同步的程序不具备上述内存模型特征:

  1. 不保证单线程内操作按程序顺序执行(重排序);
  2. 不保证所有线程看到一致的操作执行顺序(内存可见性,指令重排序等);
  3. 不保证对64位long或double型变量写操作具有原子性(JDK5之前读/写long或double不具备原子性,JDK5开始读具备原子性),而顺序一致性模型保证对所有内存读/写操作都具有原子性。

Java通过happens-before来指定两个操作之间的执行顺序,保证某个操作一定先发生于另一个操作。对程序员来说确保了编程时内存模型的可靠性;对于编译器和处理器而言,对其优化重排序起到约束作用。happens-before有且只有以下八种:

  1. 程序顺序规则:同一个线程前面的操作happens-before其后面任意操作;
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁(锁的内存语义);
  3. volatile变量规则:对volatile变量的写,happens-before后续任意对这个变量的读(volatile内存语义);
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C;
  5. start()规则:线程A执行ThreadB.start(),那么线程A的ThreadB.start() happens-before线程B中任何操作(线程A在ThreadB.start()之前对共享变量所做的修改,在线程B开始执行后都将对其可见);
  6. join()规则:线程A执行ThreadB.join(),那么线程B的任何操作happens-before线程A从ThreadB.join()操作成功返回(线程A执行ThreadB.join()并成功返回后,线程B的任何操作对线程A可见);
  7. 线程中断规则:调用线程A的interrupt()方法happens-before线程A检测到中断事件的发生;
  8. 对象终结原则:对象的初始化完成(构造函数执行完成)happens-before于finalize()方法的开始

注意:A happens-before B不代表A比B先执行,比如下面的A、B,虽然A happens-before B,但由于重排序,B可能比A先执行。因为A的执行结果不需要对B可见。

final的内存语义:写final域的重排序规则:在对象引用为任何线程可见之前,对象的final域已经被正确初始化过了(构造函数)。普通域则可能出现对象引用为线程可见,但该域还未被初始化;读final域的重排序规则:先读包含此final域的对象引用,再读此final域;而针对普通域而言,可能出现未获取对象引用之前的错误读取操作。

主要参考文献:《Java并发编程的艺术》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JMMJava Memory Model(Java内存模型)的缩写,它定义了Java程序中多线程并发访问共享变量时的行为规范。JMM的设计目的是为了保证多线程程序在不同的硬件和操作系统平台上的可移植性,并提供一定的可见性和原子性保证。 排序是指编译器和处理器对指令的执行顺序进行优化的一种技术。由于指令的排序,原本在代码中的先后顺序可能被打乱,但是这种排序不会影响单线程程序的执行结果。然而对于多线程程序来说,排序可能会导致线程安全问题。 可见性是指当一个线程对共享变量进行了修改后,其他线程是否能够立即看到这个修改。在JMM中,由于为了性能考虑,对于普通的共享变量,JVM会对读写操作进行排序,从而可能导致一个线程修改了共享变量后,其他线程无法立即看到这个修改的结果。为了保证可见性,我们可以使用volatile关键字来修饰共享变量,强制线程对共享变量的读写操作都通过主内存进行,从而解决了可见性问题。 原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行。在多线程程序中,对于一些涉及到多步操作的操作,如果没有保证原子性,可能会导致线程安全问题。为了保证原子性,可以使用synchronized关键字或者Lock来进行同步,保证任意时刻只有一个线程可以执行这些操作,从而解决了原子性问题。 总之,JMM排序、可见性和原子性是为了解决多线程程序中的线程安全问题而提出的。排序可能导致程序执行结果和预期不符,可见性问题可能导致线程无法看到其他线程对共享变量的修改,原子性问题可能导致操作只完成了一部分而不是全部。通过合理地使用volatile关键字和同步机制,可以有效地解决这些问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值