您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

java内存模型

时间:2023-11-07 13:58:59  来源:今日头条  作者:代码小人物

介绍

JAVA 内存模型 (JMM) 是 Java 并发性的基石。它定义了线程如何通过内存进行交互以及对内存操作强制执行哪些规则。对于编写多线程应用程序的开发人员来说,了解 JMM 对于创建高效、无错误的程序至关重要。

在这篇文章中,我们将深入研究 JMM 并为开发人员揭开其复杂性。

了解 Java 内存模型

Java 内存模型解释

Java 内存模型 (JMM) 作为抽象层,规定 Java 程序如何与内存交互,尤其是在多线程环境中。它是 Java 语言规范的一部分,描述了线程和主内存如何通信。

JMM 解决了并发执行带来的挑战,例如缓存一致性、内存一致性错误、线程争用以及编译器和处理器的指令重新排序。通过设置一个定义和预测并发程序行为的框架,确保跨不同平台和 CPU 与内存的可预测且统一的交互。

线程和主内存交互

在Java中,程序创建的每个线程都有自己的堆栈,其中存储局部变量和调用信息。然而,线程并不是孤立的;他们经常需要通信、共享对象和变量。这种通信通过主内存进行,主内存保存堆和方法区域。

JMM 描述了一个线程对共享数据(存储在堆或方法区域中)所做的更改如何以及何时对其他线程可见。这里的主要挑战是确保线程具有共享数据的最新视图,出于性能原因,这些数据可能会本地缓存在线程的堆栈中。

内存一致性错误

当不同线程对相同数据的视图不一致时,就会出现内存一致性错误。如果没有适当的内存模型,由于线程调度和可以重新排序指令的编译器优化的不可预测性,构建线程如何通过内存交互的模型几乎是不可能的。

JMM 通过提供一组称为“hAppens-before”的规则来帮助防止这些错误,这些规则规定了内存操作(例如读取和写入)的排序方式。

可见性和排序

可见性和排序是 JMM 提出的两个主要概念:

  • 可见性是指线程查看多个线程共享的变量的最新值的能力。如果没有显式同步,就无法保证一个线程会看到另一个线程已修改的变量的最新值。
  • 顺序是指变量发生更改的顺序。除非使用显式同步结构,否则 JMM 不会强制跨线程执行严格的操作顺序。

可见性和顺序对于创建线程安全应用程序都至关重要。如果一个线程更新的值对于应该对该更新值执行操作的另一线程不可见,则程序的行为可能会不可预测。类似地,如果操作不按顺序执行,则可能会导致线程作用于过时的数据。

happens-before

Java 内存模型的核心是“happens-before”关系。这个原则就是Java中保证内存一致性的规则手册。“happens-before”关系提供了多线程环境中变量操作(读取和写入)的部分顺序。

以下是一些构成 JMM 内线程交互基础的关键happens-before规则:

  1. 程序顺序规则:线程中的每个操作发生在该线程中按程序顺序出现的每个操作之前。
  2. 监视器锁规则:监视器锁上的解锁发生在同一监视器锁上的每个后续锁定之前。
  3. volatile变量规则:对volatile字段的写入发生在同一字段的每次后续读取之前。
  4. 线程启动规则:对线程上的 Thread.start 的调用发生在已启动线程中的任何操作之前。
  5. 线程终止规则:线程中的任何操作都发生在任何其他线程检测到该线程已终止之前,无论是从 Thread.join 成功返回还是 Thread.isAlive 返回 false。
  6. 中断规则:一个线程在另一个线程上调用中断发生在被中断线程检测到中断之前(通过抛出 InterruptedException,或者通过调用 isInterrupted 或 Interrupted)。
  7. 传递性:如果 A 发生在 B 之前,并且 B 发生在 C 之前,则 A 发生在 C 之前。

理解和应用这些规则可确保程序在并发环境中的行为可预测。这些规则是避免内存一致性错误、确保可见性和维护操作正确顺序的关键。

 Java 内存模型应用

了解 JMM 的细节使开发人员能够编写安全且可扩展的并发应用程序。同步原语(synchronized、volatile等)、原子变量、并发集合的正确使用都植根于JMM。例如,了解volatile变量提供的保证有助于防止过度使用同步,从而提高应用程序的性能和可扩展性。

此外,在 JMM 的上下文中,我们还考虑“as-if-serial”语义,它保证单个线程中的执行行为就像所有操作都按照它们在程序中出现的顺序执行一样 — 即使编译器实际上可能会在幕后重新排序指令。
 

Java 内存模型组件

Java 内存模型 (JMM) 是 Java 并发框架的基石,定义了 Java 线程和内存之间的交互。它指定一个线程所做的更改如何以及何时对其他线程可见,从而确保并发执行的可预测性。让我们检查一下构成 JMM 的关键组件。

共享变量

在Java中,被多个线程访问的变量是共享变量。这些变量存储在堆中,堆是内存的共享区域。如果处理不当,共享变量可能会成为内存一致性错误的根源。JMM 控制这些共享变量的更改如何在线程内存和主内存之间传播。

volatile变量

Java中的关键字volatile用于将Java变量标记为“正在存储在主存中”。更准确地说,这意味着对 volatile变量的每次读取都将从主内存中读取,而不是从线程的本地缓存中读取,并且对volatile变量的每次写入都将写入主内存,而不仅仅是线程的本地缓存。

public class SharedObject {
    private volatile int sharedVariable;

    public void updateValue(int newValue) {
        sharedVariable = newValue;
    }

    public int getValue() {
        return sharedVariable;
    }
}

volatile 关键字保证一个线程中所做的更改对另一个线程的可见性。它是同步的轻量级替代方案,尽管它不提供原子性或互斥性。

同步块

同步是Java中确保线程安全的主要工具之一。同步块或方法一次只允许一个线程执行一段代码,确保只有一个线程可以访问正在同步的资源。

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

Synchronized 关键字确保一个线程所做的更改对其他线程可见,并且还可以防止多个线程同时执行代码块。

Final

在 Java 中,final 关键字可用于将字段标记为不可变。一旦最终字段被初始化,就不能更改。这种不变性提供了固有的线程安全性,因为无需担心多个线程修改该值。 JMM 保证设置最终字段的构造函数的效果对于获得该对象引用的任何线程都是可见的。

public class ImmutableValue {
    private final int value;

    public ImmutableValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

在这种情况下,一旦创建了 ImmutableValue 实例,value 字段就无法更改,因此不需要进一步同步。

Happens-Before规则

使用 Java 内存模型

同步访问共享数据

使用 Java 内存模型时,最常见的任务是同步对共享数据的访问。这确保一次只有一个线程可以访问代码的关键部分,从而降低内存一致性错误的风险。 Synchronized关键字可用于锁定一个对象,以便同一时间只有一个线程可以访问同步代码块或方法。

这是一个使用同步的示例:

public class Account {
    private int balance;

    public synchronized void deposit(int amount) {
        balance += amount;
    }

    public synchronized int getBalance() {
        return balance;
    }
}

在上面的示例中,deposit 方法和 getBalance 方法在 Account 类的实例上同步。这意味着,如果一个线程正在执行 Deposit 方法,则在第一个线程退出同步块之前,其他线程都无法执行 Deposit 或 getBalance。

volatile变量的可见性

volatile变量是使用 JMM 的另一个关键方面。当一个字段被声明为volatile时,编译器和运行时会被通知该变量是共享的,并且对该变量的操作不应与其他内存操作重新排序。volatile变量可用于确保一个线程所做的更改对其他线程的可见性。

public class Flag {
    private volatile boolean shutdownRequested;

    public void shutdown() {
        shutdownRequested = true;
    }

    public void doWork() {
        while (!shutdownRequested) {
            // perform work
        }
    }
}

在此示例中,shutdownRequested 标志是volatile的,这可确保 shutdown 方法对 shutdownRequested 所做的更改对于正在检查该值的任何其他线程立即可见。

原子变量的并发逻辑

Java 在
java.util.concurrent.atomic 包中提供了一组原子变量(例如 AtomicInteger、AtomicLong、AtomicBoolean 等),它们使用高效的机器级并发构造。

这些可用于在不使用同步的情况下安全地对单个变量执行原子操作。

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

原子变量通常是从多个线程访问的计数器和标志的更好替代方案。

数据共享的并发集合

为了在线程之间共享数据集合,Java 提供了线程安全的变体,例如 ConcurrentHashMap、CopyOnWriteArrayList 和 BlockingQueue。这些集合负责内部同步,并提供比同步标准集合更高的并发性能。

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentCache {
    private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();

    public void putInCache(String key, Object value) {
        cache.put(key, value);
    }

    public Object getFromCache(String key) {
        return cache.get(key);
    }
}

使用并发集合可以显着简化同步访问集合数据的任务。

处理线程干扰和内存一致性错误 线程干扰和内存一致性错误是开发人员在处理共享数据时面临的两个主要问题。为了避免这些问题,必须了解先发生关系并正确同步对共享变量的访问。使用同步、volatile变量、原子变量和并发集合可以缓解这些问题。

JMM常见问题及解决方案

线程干扰

问题:当多个线程对共享数据进行操作时,一个线程的操作可能会干扰另一个线程的操作,从而导致错误的结果。

解决方案:使用同步机制(如同步块、
java.util.concurrent.locks 中的锁或原子变量)来确保一次只有一个线程可以访问数据。

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

内存一致性错误

问题:一个线程对共享变量所做的更改可能对其他线程不可见,从而导致内存一致性错误。

解决方案:使用同步块、易失性变量或不可变对象的最终字段建立happens-before关系。

public class SharedFlag {
    private volatile boolean flag = false;

    public void setFlag() {
        this.flag = true;
    }

    public boolean checkFlag() {
        return flag;
    }
}

死锁

问题:当两个或多个线程永久阻塞,每个线程都等待另一个线程释放锁时,就会发生死锁。

解决方案:避免死锁的一种常见策略是对锁进行排序,并始终以相同的预定义顺序获取多个锁。

锁饥饿

问题:当一个或多个线程永远被拒绝访问共享资源或锁时,就会发生饥饿,这通常是因为其他线程占用了资源。

解决方案:使用公平锁(例如将公平参数设置为 true 的 ReentrantLock)或其他机制来确保所有线程都有机会执行。

import java.util.concurrent.locks.ReentrantLock;

public class FAIrLockExample {
    private final ReentrantLock lock = new ReentrantLock(true);

    public void fairLockMethod() {
        lock.lock();
        try {
// 访问受该锁保护的资源
        } finally {
            lock.unlock();
        }
    }
}

活锁

问题:活锁是一种线程未被阻塞的情况——它们只是太忙于相互响应而无法恢复工作。

解决方案:检测活锁情况并实施退避策略,让线程有机会逃脱活锁状态。

假共享

问题:当不同处理器上的线程修改驻留在同一缓存行上的变量时,会发生错误共享,从而导致不必要的缓存刷新和失效。

解决方案:一种解决方案是填充数据结构,以确保常用访问的共享变量不共享缓存行。

共享对象的可见性

问题:由于缓存或重新排序,线程可能看不到对象引用或基元的最新值。

解决方案:对不涉及复合操作的简单标志和引用使用 volatile,确保对变量的写入立即跨线程反映。

非原子复合操作

问题:读取-修改-写入操作(例如递增计数器)不是原子操作,并且在多个线程访问时可能会导致状态不一致。

解决方案:使用
java.util.concurrent.atomic 包中的原子类或同步复合操作。

import java.util.concurrent.atomic.AtomicInteger;

public class SafeCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

总结

Java 内存模型是 Java 的一个复杂部分,需要深入理解才能编写正确且高效的并发程序。开发人员应努力深入理解 JMM,以避免并发问题并构建健壮的应用程序。通过遵循最佳实践并理解模型的核心组件和原则,我们可以利发挥出Java 并发编程的真正威力。



Tags:java内存   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  Search: java内存  点击:(68)  评论:(0)  加入收藏
java内存模型
介绍Java 内存模型 (JMM) 是 Java 并发性的基石。它定义了线程如何通过内存进行交互以及对内存操作强制执行哪些规则。对于编写多线程应用程序的开发人员来说,了解 JMM 对于...【详细内容】
2023-11-07  Search: java内存  点击:(370)  评论:(0)  加入收藏
为什么需要Java内存模型
在日常的程序开发中,我们经常会遇到为共享变量赋值的场景。假设有一个线程为整型共享变量count赋值(count=9527),那么其他读取该共享变量的线程在什么情况下才能获取到变量值为9...【详细内容】
2023-11-07  Search: java内存  点击:(325)  评论:(0)  加入收藏
如何避免Java内存泄漏,来看看这个
引言:在Java应用程序开发中,内存泄漏是一个常见而严重的问题。本文将帮助Java开发人员和软件工程师了解内存泄漏的危害,并提供解决方案。了解内存泄漏: 内存泄漏是指分配的内存...【详细内容】
2023-10-30  Search: java内存  点击:(242)  评论:(0)  加入收藏
深入理解Java内存模型:探索线程与内存的交互
在多线程编程中,了解Java内存模型(Java Memory Model,简称JMM)是非常重要的。Java内存模型定义了线程与内存之间的交互规则,确保多线程程序的正确性和可见性。下面将深入探索Java...【详细内容】
2023-08-25  Search: java内存  点击:(273)  评论:(0)  加入收藏
Java内存泄漏、性能优化、宕机死锁的N种姿势
导读本文介绍Java诸多优化实例:第一,排查堆上、堆外内存泄露;第二,使用arthas、jaeger、tcpdump、jstack做性能优化;第三,排查进程异常退出的原因,如被杀、System.exit、Java调用的...【详细内容】
2020-11-25  Search: java内存  点击:(253)  评论:(0)  加入收藏
万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势
导读本文介绍Java诸多优化实例:第一,排查堆上、堆外内存泄露;第二,使用arthas、jaeger、tcpdump、jstack做性能优化;第三,排查进程异常退出的原因,如被杀、System.exit、Java调用的...【详细内容】
2020-08-11  Search: java内存  点击:(259)  评论:(0)  加入收藏
图解Java内存区域
Java是一座围城,Java开发不需要像C、C++开发人员那样,维护每个对象从开始到终结的职责。因为Java虚拟机会帮助我们完成这些职责,但是一旦发生内存泄漏和溢出,就需要我们排查。 J...【详细内容】
2020-03-06  Search: java内存  点击:(288)  评论:(0)  加入收藏
java内存泄漏5种情况及总结
内存泄漏定义(memory leak):一个不再被程序使用的对象或变量还在内存中占有存储空间。一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。 内存溢出 out o...【详细内容】
2020-02-24  Search: java内存  点击:(327)  评论:(0)  加入收藏
Java内存结构
运行时数据区域Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。Java虚拟机所管理的内存包括如下几个部分: 程序计数器程序计数器是一块较...【详细内容】
2020-02-23  Search: java内存  点击:(287)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(21)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(24)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(56)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(68)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(72)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(88)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(105)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(95)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(74)  评论:(0)  加入收藏
站内最新
站内热门
站内头条