Java内存模型(JMM)详解

2023-12-30 10:51:50

1. 介绍

1.1 JMM概述

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

1.2 主要概念

  • 共享变量:在多个线程之间可见的变量,例如对象的字段、静态变量等。
  • 主内存:是线程间共享的内存区域,所有线程都可以访问。主内存存储了共享变量的原始副本。
  • 工作内存:是线程私有的内存区域,每个线程有自己的工作内存。工作内存中存储了主内存中共享变量的副本。

1.3 JMM 的作用

  • 保证可见性:确保一个线程对共享变量的修改对其他线程是可见的。即当一个线程修改了某个共享变量时,其他线程能够立即看到这个变化。

  • 保证原子性:确保一些特定操作不会被中断,使得它们在多线程环境中的执行结果与在单线程环境中的执行结果一致。例如,一个操作要么完整地执行,要么不执行,不会出现部分执行的情况。

  • 保证有序性:规定程序中的代码执行顺序,禁止编译器和处理器对代码进行重排序优化。

1.4 JMM 的特性

  • 内存可见性:当一个线程修改了共享变量的值,其他线程能够立即看到这个变化。

  • 指令重排序:JMM允许编译器和处理器对指令进行重排序,但是在多线程环境下保证最终的执行结果与串行一致性执行的结果一致。

  • happens-before:提供了一种 happens-before 关系,即前一个操作的结果对于后一个操作是可见的。这种顺序性规则用于确保正确的内存可见性。

1.5 并发编程与JMM的关系

  • 并发编程挑战:并发编程中常见的挑战包括竞态条件(Race Conditions)、死锁(Deadlocks)、内存可见性问题等。多线程并发访问共享资源时,会存在数据不一致、线程安全性等问题。

  • JMM关联:JMM定义了Java程序中多线程并发访问共享内存的规范。它规定了共享变量的可见性、有序性、原子性等特性,从而解决了并发编程中的一些问题。JMM提供了保证多线程间共享数据正确访问的规则和约束。

2. 主内存与工作内存

????????Java内存模型(JMM)中的主内存和工作内存是为了描述多线程并发访问共享变量时的内存交互情况而定义的两个概念。

2.1 主内存(Main Memory)

  • 主内存是所有线程共享的内存区域。
  • 所有的共享变量都存储在主内存中。
  • 主内存是线程之间的公共资源池,所有线程都可以访问它。
  • 主内存中存储了对象实例、类信息、静态变量等数据。

2.2 工作内存(Working Memory)

  • 工作内存是每个线程独有的内存空间。
  • 每个线程都有自己的工作内存。
  • 工作内存中存储了线程运行时使用到的变量的副本。
  • 线程的操作(读写共享变量)都在工作内存中进行。

?

2.3 内存交互操作

????????JMM规定了主内存和工作内存之间的交互操作,主要涉及两种操作:

  • 读取操作:当线程需要使用共享变量的值时,它会先从主内存中将变量的值复制到自己的工作内存中,然后执行操作。

  • 写入操作:当线程修改了共享变量的值后,它会先在自己的工作内存中修改这个变量,然后将变更同步到主内存中。

2.4 实现线程间通信

????????主内存与工作内存的概念实现了线程间的通信机制,保证了多线程之间对共享变量的可见性、有序性和原子性操作。JMM规定了线程如何与主内存进行交互,从而保证了并发编程的正确性和稳定性。

?下面用一张图来看下JVM整体结构及内存模型

3. Happens-Before原则

????????Happens-Before(先行发生,以下简称HB)是Java内存模型(JMM)中的一种规则或原则,用于确保多线程程序中操作的顺序性。它描述了在多线程环境中,对共享变量的操作顺序规则,从而确保正确的内存可见性和顺序性。

3.1 Happens-Before的定义

????????Happens-Before原则规定了一系列操作之间的先后顺序关系。如果一个操作Happens-Before于另一个操作,那么前一个操作对于后一个操作的结果是可见的。

3.2 Happens-Before的规则

  • 程序顺序规则(Program Order Rule):在一个线程内,按照程序代码的先后顺序执行的操作,会产生Happens-Before的关系。

  • 锁定规则(Monitor Lock Rule):对于一个锁的解锁操作Happens-Before于后续对于该锁的加锁操作。

  • volatile变量规则:对一个volatile变量的写操作Happens-Before于后续对这个变量的读操作。

  • 传递性规则(Transitivity):如果操作A Happens-Before操作B,操作B Happens-Before操作C,则操作A Happens-Before操作C。

  • 线程启动规则(Thread Start Rule):线程的启动操作Happens-Before于新线程的所有操作。

  • 线程终止规则(Thread Termination Rule):线程的所有操作Happens-Before于其他线程检测到该线程终止。

3.3 应用场景

  • 保证顺序性:Happens-Before规则保证了对共享变量的写操作先于对该变量的读操作,保证了操作的顺序性。

  • 保证可见性:由于Happens-Before关系可以保证操作的先后顺序,因此一个线程对变量的修改操作对于其他线程来说是可见的。

  • 内存同步:Happens-Before规则提供了内存同步的机制,保证了多线程并发操作时内存数据的正确性和一致性。

Happens-Before原则是Java并发编程中重要的理论基础,它确保了多线程环境下对共享变量的操作顺序和可见性,帮助程序员编写正确、可靠的多线程并发程序。

4. Volatile关键字

volatile 是Java关键字,用于声明变量。它具有以下作用和特性:

4.1 作用和特性

  1. 可见性:保证变量的修改对其他线程是可见的。
  2. 禁止重排序:防止编译器和处理器对指令进行重排序优化。

4.2 保证可见性

  • 当一个变量被 volatile 关键字修饰时,对该变量的写操作会立即刷新到主内存,并且对该变量的读操作也会直接从主内存中读取,而不是从线程的工作内存中读取。
  • 这样可以确保一个线程对变量的修改对其他线程是立即可见的,避免了多线程环境下由于工作内存和主内存数据不一致而引发的问题。

4.3 禁止重排序

  • volatile 关键字可以禁止编译器和处理器对被修饰变量的读写操作进行重排序优化。
  • 这意味着对 volatile 变量的写操作不会被重排序到其他操作之后,也不会将其之前的操作重排序到它之后。
  • 同样,对 volatile 变量的读操作也不会被重排序到它之前的其他操作之后。

4.4 使用注意事项

  • volatile 适用于对变量的写操作不依赖变量的当前值,或者仅用于单一线程写、多线程读的情况。
  • 适用于标志位等频繁被读写的变量,而不适用于复合操作,如 i++ 操作。
  • volatile 并不能保证原子性操作,因此对于多线程并发修改同一变量时可能需要额外的同步控制。

4.5 Volatile的使用场景和注意事项

使用场景:
  • 状态标志位:在多线程中用作标识位,例如线程之间的控制标志位,确保不同线程之间状态的可见性。

  • 单一写、多线程读:一个变量被多个线程读取但只有一个线程写入,可以使用volatile来保证可见性。

  • Double-checked locking:在单例模式的双重检查锁定中,如果要求该单例实例对所有线程是可见的,可以将单例实例声明为volatile

何时使用Volatile关键字:
  • 当需要保证对一个变量的修改对其他线程是立即可见的,并且变量的写操作不依赖于当前值时,可以考虑使用volatile关键字。

  • 如果多个线程共享一个变量,其中一个线程修改了该变量,需要确保其他线程能够及时看到该变量修改后的值时,可以选择使用volatile

局限性和注意事项:
  • 不保证原子性volatile只能保证可见性,不能保证操作的原子性。对于复合操作(例如i++),需要额外的同步控制才能确保原子性。

  • 不适用于线程安全的计数器:如果需要在多线程环境下进行自增等操作,应该使用Atomic类提供的原子操作而不是volatile

  • 不保证操作的有序性:虽然volatile可以防止指令重排序,但并不能保证复合操作的有序性。

  • 并不能替代锁volatile不能替代锁来确保原子性和线程安全,它适用于一些特定场景下的轻量级同步需求。

  • 谨慎使用volatile的使用需要谨慎,不当的使用可能导致意想不到的结果,特别是对于复合操作和线程安全性的要求。

5. JMM与性能优化

????????通过对Java内存模型(JMM)的理解,可以优化并发程序,提高性能和可靠性。以下是一些常见的性能优化技巧和最佳实践:

5.1. 减少锁竞争

  • 粒度控制:减小锁的粒度,使得锁的持有时间尽可能短,避免长时间的锁竞争。

  • 分离锁:如果不同的数据可以使用不同的锁,可以将锁分离以减少不同数据间的锁竞争。

5.2. 使用局部变量

  • 局部变量优于共享变量:尽可能使用线程本地的局部变量,减少对共享变量的访问。

5.3. 使用非阻塞算法

  • CAS(比较并交换):使用CAS替代锁,例如Atomic类提供的原子操作。

5.4. 合理使用线程池

  • 线程池调优:根据实际场景调整线程池的大小,避免线程数量过多或过少导致的资源浪费或性能瓶颈。

5.5. 避免过度同步

  • 避免不必要的同步:只对需要保护的共享数据进行同步,避免过度同步。

5.6. 缓存优化

  • 缓存策略:合理选择缓存数据的策略,避免缓存过期导致的大量并发请求。

5.7. 并发数据结构

  • 使用并发容器:使用ConcurrentHashMapConcurrentLinkedQueue等并发数据结构,避免手动加锁。

5.8. 内存优化

  • 对象池:复用对象,减少对象的创建和销毁,降低内存开销。

  • 减少内存泄漏:确保不再使用的对象能够被垃圾回收,避免内存泄漏问题。

5.9. 测试与监控

  • 压力测试:对并发程序进行压力测试,发现并发瓶颈,并针对性地优化。

  • 性能监控:使用工具监控程序的性能指标,定位性能瓶颈,例如使用VisualVM、Arthas等进行性能分析。

6. 总结

????????Java内存模型(JMM)在Java并发编程中具有重要性,它定义了多线程并发访问共享内存时的规范,为开发高性能、可靠性的并发程序提供了必要的保证和指导。最后我们再总结一下Java内存模型的作用

  • 保证可见性和一致性

    JMM规定了共享变量的读写规则,确保一个线程对共享变量的修改对其他线程是可见的,避免了数据不一致的情况。
  • 确保顺序性和有序性

    JMM通过Happens-Before规则规定了操作的顺序关系,保证了程序中操作的执行顺序,避免了指令重排序带来的问题。
  • 提供原子性保证

    JMM通过synchronized、volatile等机制提供了对共享变量操作的原子性保证,避免了线程安全问题。
  • 避免竞态条件和死锁

    JMM的规范和约束减少了竞态条件和死锁等并发编程中常见的问题,提高了程序的稳定性和可靠性。
  • 指导并发程序设计

    对JMM的理解有助于开发人员设计更安全、更高效的并发程序。合理使用同步机制、了解内存交互规则可以更好地优化程序性能。
  • 对高性能程序的重要性

    在需要高性能、高并发的场景下,对JMM的理解和合理运用可以避免潜在的并发问题,提高程序的性能和稳定性。

文章来源:https://blog.csdn.net/EverythingAtOnce/article/details/135302248
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。