深入浅出Java多线程(六):Java内存模型

news2024/10/5 1:44:08

引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第六篇内容:Java内存模型。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!

在并发编程中,有两个关键问题至关重要,它们是线程间通信机制和线程间同步控制。

线程间通信机制

线程间通信是指在一个多线程程序中,不同线程之间如何有效地交换信息。在Java内存模型(JMM)采用的共享内存并发模型中,线程间的通信主要是通过共享变量来实现的。每个线程可以读取或修改这些存储在堆内存中的共享变量,从而传递状态或数据给其他线程。例如:

class SharedData {
    public volatile int sharedValue;
}

public class ThreadCommunication {
    public static void main(String[] args) {
        SharedData data = new SharedData();
        Thread threadA = new Thread(() -> {
            data.sharedValue = 10; // 线程A更新共享变量
        });

        Thread threadB = new Thread(() -> {
            while (data.sharedValue == 0) {} // 线程B等待共享变量被更新
            System.out.println("Thread B sees updated value: " + data.sharedValue);
        });

        threadA.start();
        threadB.start();

        try {
            threadA.join(); // 确保线程A完成更新
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个案例中,sharedValue 是一个共享变量,线程A对其进行了修改,而线程B则依赖于该变量的值进行后续操作。为了确保线程间通信的正确性,这里使用了 volatile 关键字来保证变量的可见性和禁止指令重排序。

线程间同步控制

线程间同步则是指控制不同线程间操作发生的相对顺序,以避免数据不一致和竞态条件等问题。在Java中,同步控制主要通过以下方式实现:

  1. synchronized关键字:它可以修饰方法或代码块,确保同一时间只有一个线程能访问被保护的资源。如下所示:
class Counter {
    private int count = 0;

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

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

在这个例子中,increment()getCount() 方法都被 synchronized 修饰,这样在同一时刻只能有一个线程执行这两个方法之一,防止了并发环境下计数器值的错误累加。

  1. Lock接口及其实现类:除了内置的synchronized机制,Java还提供了更灵活的Lock接口如ReentrantLock,它允许更多的同步语义,比如尝试获取锁、可中断获取锁以及公平锁等。
  2. volatile关键字:虽然主要用于提供内存可见性,但其也能间接起到一定的同步作用,即当一个线程修改了volatile变量时,其他线程能够立即看到这个新值。

综上所述,在Java并发编程中,线程间通信与同步控制相辅相成,共同构建了一个安全高效的并发环境。通过合理地利用Java内存模型提供的机制,开发者可以确保在多线程环境中,各个线程之间的数据交换有序且可靠。

并发模型对比


在并发编程领域,有两种主要的并发模型:消息传递并发模型和共享内存并发模型。Java多线程编程采用了共享内存并发模型,这一选择对理解Java内存模型(JMM)至关重要。

Untitled.png

消息传递并发模型

消息传递模型中,线程之间的通信和同步是通过发送和接收消息来实现的。每个线程拥有独立的本地状态,并通过将数据封装在消息中发送给其他线程来交换信息。在这种模型下,线程之间不直接共享数据,因此不存在竞争条件或同步问题。Erlang等语言中的Actor模型就是一个典型的消息传递并发模型实例。

-module(my_actor).
-export([start_link/0, ping/0]).

start_link() ->
    register(actor_name, spawn(fun() -> loop([]) end)).

ping() ->
    actor_name ! {self(), ping}.

loop(Msgs) ->
    receive
        {From, ping} ->
            From ! pong,
            loop(Msgs);
        _Other ->
            loop([Msg | Msgs])
    end.

在此Erlang示例中,actor_name 是一个进程(即线程),它通过接收并响应消息来进行工作,而不是直接读写共享变量。

共享内存并发模型

而在Java中采用的共享内存模型,则允许线程访问相同的内存区域——堆区,其中包含的共享变量可以被多个线程同时读写。这种模型下,线程间通信是通过对共享变量进行读写操作间接完成的。然而,由于共享数据,这就带来了潜在的数据一致性问题,如竞态条件、死锁以及可见性问题。为了保证线程间的正确交互,Java内存模型定义了一套规则和机制。

public class SharedCounter {
    private volatile int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SharedCounter counter = new SharedCounter();
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                counter.increment();
            }
        });

        Thread threadB = new Thread(() -> {
            while (true) {
                System.out.println(counter.getCount());
            }
        });

        threadA.start();
        threadB.start();

        threadA.join();
    }
}

在这个Java示例中,两个线程同时访问SharedCounter类的共享变量count,为了确保线程安全和可见性,我们使用了volatile关键字修饰该变量。Java内存模型通过主内存与各线程私有本地内存间的抽象关系以及内存屏障技术,保障了线程间共享变量的更新能够及时传播到所有线程。

Java选择共享内存并发模型的原因在于其简洁性和高效性,尤其是对于基于对象和引用透明性的程序设计而言。尽管存在潜在的并发问题,但通过提供诸如synchronizedvolatile以及更高层次的并发工具如java.util.concurrent包下的各种锁机制和原子类等,Java提供了丰富的工具来管理和控制共享内存环境下的并发行为,使得开发者能够编写出高效的并发代码。

Java内存模型抽象结构解析


运行时数据区划分

Untitled 1.png

在Java虚拟机(JVM)的运行时环境中,内存被划分为多个区域以支持程序的执行。其中,线程私有的内存区域包括程序计数器、虚拟机栈以及本地方法栈,而堆和方法区则是所有线程共享的内存区域。

  • :每个线程都有自己的栈空间,用于存储局部变量、方法调用时的上下文信息(如返回地址、临时变量等)。由于栈是线程私有的,因此线程间不涉及共享和可见性问题。
public class StackExample {
    public void localVariableVisibility() {
        int localVar = 10; // 局部变量存储在线程栈中,对其他线程不可见
    }
}

  • :堆内存是所有线程共享的区域,主要存储对象实例及数组。当创建一个类的对象时,其对象实例就分配在堆内存上,这些实例变量对所有能够访问到该对象的线程都是可见的。

堆内存中的内存不可见性原因

现代计算机系统为了提高性能,普遍采用了高速缓存技术,CPU有自己的缓存层级,包括L1、L2、L3等高速缓存。当线程A修改了堆内存中的共享变量时,这个更新可能只反映在了线程A所在的CPU缓存中,而不是立即同步到主内存或其他线程所在的CPU缓存中。这就是为什么即使是在共享内存区域——堆内存在多线程环境下也可能出现内存不可见性的问题。

public class CacheCoherenceIssue {
    private static volatile int sharedValue = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                sharedValue++;
            }
        });

        Thread threadB = new Thread(() -> {
            while (sharedValue == 0); // 如果没有volatile,可能会陷入循环无法退出
            System.out.println("Thread B observed: " + sharedValue);
        });

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();
    }
}

上述代码中,如果没有使用volatile关键字修饰sharedValue,线程B可能无法观察到线程A对共享变量的更新,因为这种更新可能未及时传播至主内存或线程B的工作内存。

Java内存模型(JMM)详解

Java内存模型(JMM)是一种抽象概念,它定义了Java程序中各种变量的访问规则,尤其是针对堆内存中的共享变量。JMM确保了并发环境下的原子性、有序性和可见性:

  1. 主内存与本地内存的关系:根据JMM的描述,所有共享变量都存储于主内存中,每个线程具有自己的本地内存,保存了该线程读写共享变量的副本。线程间的通信必须通过主内存进行,即线程先将本地内存中的共享变量刷新回主内存,然后其他线程从主内存读取最新值并更新到自己本地内存的过程。
  2. 内存操作的顺序保证:JMM通过内存屏障来控制指令重排序,从而确保特定操作的顺序性,比如volatile变量的写后读操作不会被重排序。
  3. 内存可见性的实现:JMM提供了一系列规则和机制来保证不同线程对共享变量修改的可见性,例如volatile变量的写会强制刷入主内存,并使其他线程对该变量的读失效,进而从主内存重新加载。

综上所述,Java内存模型在多线程编程中扮演着核心角色,通过规范和约束线程如何访问和更新共享变量,有效地解决了并发环境下的内存一致性问题。

Java内存模型与Java内存区域的关系


两者区别

Java内存模型(JMM)和Java运行时内存区域是两个不同的概念层次,它们在描述并发编程的内存行为时有着各自的侧重点:

  • Java内存模型(JMM):从抽象层面定义了线程之间如何通过主内存进行交互以及如何保证数据的一致性和有序性。JMM关注的是对共享变量访问规则的规范,比如可见性、原子性和有序性,它是一组关于程序中所有变量访问操作的协议或约定。
  • Java运行时内存区域:这是更为具体的概念,指Java虚拟机(JVM)在运行Java程序时实际划分的内存区域,包括堆、栈、方法区、程序计数器等。这些内存区域分别存储着对象实例、局部变量、类信息、线程上下文等不同类型的内存数据,并且各区域具有不同的生命周期和管理策略。

联系与映射

尽管JMM与Java运行时内存区域在概念上有所差异,但它们之间存在着密切的联系和映射关系:

  1. 主内存与共享数据区域
    在JMM中,主内存对应于Java运行时内存区域中的堆和方法区。堆存放了Java对象实例,即多线程可以共享的对象数据;而方法区则包含了类的元数据和静态变量等,这些也是全局可访问的信息,因此它们都属于“主内存”的范畴。
  2. 本地内存与私有数据区域
    JMM中的本地内存实际上是一个抽象概念,涵盖了缓存、写缓冲区、寄存器等硬件设施,对应到Java运行时内存区域,可以理解为每个线程的私有工作空间,如程序计数器、虚拟机栈和本地方法栈。其中,虚拟机栈保存了方法调用的局部变量表,以及操作数栈等信息,这些都是严格线程私有的,符合本地内存的概念。

虽然无法直接以代码形式展示这种抽象的映射关系,但在实际编程中,我们可以观察到以下现象:

public class MemoryModelMapping {
    private static int sharedValue; // 存储在堆中,属于主内存区域
    private int threadLocalValue; // 存储在线程栈中,属于本地内存

    public void runInParallel() {
        Thread threadA = new Thread(() -> {
            sharedValue = 10; // 修改共享变量
            threadLocalValue = 20; // 修改线程局部变量
        });

        Thread threadB = new Thread(() -> {
            while (sharedValue == 0) {} // 等待共享变量更新
            System.out.println("Shared value: " + sharedValue);
        });

        threadA.start();
        threadB.start();

        try {
            threadA.join();
            threadB.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,sharedValue 变量由于被多个线程共享,它的修改需要遵循JMM的同步和可见性规则,而 threadLocalValue 变量仅在线程内部使用,不受JMM的跨线程可见性约束,其生命周期完全受限于所在线程的虚拟机栈范围。这样,我们便能直观地感受到JMM与Java运行时内存区域之间的关联和作用机制。

Java语言特性与JMM实现

volatile关键字的作用

在Java并发编程中,volatile关键字是一个重要的工具,它用于修饰共享变量,确保了该变量在多线程环境下的可见性和禁止指令重排序。当一个线程修改了volatile变量的值时,其他线程能够立即看到这个更新后的值,这是因为volatile变量的读写操作都会与主内存直接交互,并且会在必要时插入内存屏障以保证数据的一致性。

public class VolatileExample {
    private volatile int sharedValue = 0;

    public void increment() {
        sharedValue++;
    }

    public int getSharedValue() {
        return sharedValue;
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();
        Thread threadA = new Thread(example::increment);
        Thread threadB = new Thread(() -> System.out.println("Thread B sees: " + example.getSharedValue()));

        threadA.start();
        threadA.join(); // 确保线程A完成操作
        threadB.start();
    }
}

在这个例子中,sharedValue 是一个被volatile修饰的变量,线程A对其进行了递增操作,而线程B可以立即获取到最新的值,体现了volatile对于共享状态同步的重要作用。

synchronized关键字的功能

synchronized 关键字提供了原子性和可见性保障,它可以应用于方法或代码块,使得在同一时间只有一个线程能访问被保护的资源,从而有效地解决了竞态条件和数据一致性问题。

public class SynchronizedExample {
    private int counter = 0;

    public synchronized void incrementCounter() {
        counter++;
    }

    public synchronized int getCount() {
        return counter;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.incrementCounter();
            }
        });

        Thread threadB = new Thread(() -> {
            while (true) {
                if (example.getCount() >= 1000) {
                    System.out.println("Counter reached 1000");
                    break;
                }
            }
        });

        threadA.start();
        threadB.start();

        try {
            threadA.join();
            threadB.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在此示例中,synchronized 方法 incrementCounter()getCount() 保证了计数器的增量操作是原子性的,同时多个线程对counter的读写操作不会出现竞态条件,即线程B总能看到线程A对counter修改的最新结果。

内存屏障与happens-before原则

为了更深入地理解并发控制机制,Java内存模型还引入了内存屏障(Memory Barrier)的概念,这是一种硬件级别的指令,用于确保特定内存操作顺序并刷新缓存。Java编译器会根据JMM规则,在适当的时机插入内存屏障,以实现对volatile变量和其他同步原语的正确支持。

另外,Java内存模型通过happens-before原则来简化程序员理解和推理程序行为。它定义了一系列先行发生关系,比如:程序次序规则、监视器锁规则等,这些规则明确了事件之间的执行顺序,如果A happens-before B,那么线程A对共享变量的修改对于线程B来说一定可见。

例如:

public class HappensBeforeExample {
    private static boolean flag = false;
    private static int data = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread one = new Thread(() -> {
            data = 1; // 修改数据
            flag = true; // 设置标志位
        });

        one.start();
        one.join();

        // 根据happens-before原则,由于监视器锁规则
        // 当进入同步块时,线程将看到之前对flag的修改
        synchronized (HappensBeforeExample.class) {
            if (flag) {
                System.out.println("Data seen in other thread: " + data); // 输出正确的值
            }
        }
    }
}

在这个例子中,因为synchronized 关键字遵循happens-before原则中的监视器锁规则,因此主线程在进入同步块时,可以看到之前线程one对flag的修改,进而确定data变量是否已经被正确设置。



喜欢的朋友记得点赞、收藏、关注哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2189065.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

简码短链测试用例设计报告

文章目录 1.前言2.用户模块2.1 登录2.2 注册2.3 修改个人信息2.4 退出登录 3.短链接分组模块3.1 创建短链接分组3.2 修改短链接分组3.3 删除短链接分组 4.短链接管理模块4.1 创建单个短链接4.2 批量创建短链接4.3 修改短链接信息4.4 分页查询短链接4.5 短链接跳转原始链接4.6 删…

CTK框架(十一):使用的常见问题

目录 1.MF文件路径 2.服务必须要接口类 3.插件名命名要求 4.生命周期问题 5.一个接口对多个实现注意 6.中文输出注意 7.同一插件安装注意 8.添加元数据 9.关于升级插件时遇到的问题 10.不同插件定义资源文件注意路径问题 11.安装插件 12.插件依赖 1.MF文件路径 在…

基于Springboot+VUE的二手奢侈品商城的设计与实现

一、摘要 当前&#xff0c;二手奢侈品市场持续蓬勃发展&#xff0c;吸引了越来越多的消费者。然而&#xff0c;现有的二手奢侈品交易平台在用户体验、安全性和功能方面仍存在一些问题&#xff0c;需要进一步改进。本研究旨在设计和实现一种基于Spring Boot 和 Vue 技术框架的二…

题目:最左边的数字

问题 - 1060 (hdu.edu.cn) 解题思路&#xff1a; 数字很大&#xff0c;使用科学计数法。则&#xff0c;我们需要的是a的整数位&#xff0c;最终求出a即可。 取对数&#xff1a;nlgnmlga&#xff0c;移项&#xff1a;lganlgn-m&#xff0c;接下来我们需要求m。 …

04:(寄存器开发)使用外部中断按键控制LED

寄存器开发 1、选择外部引脚配置2、上升沿触发/下降沿触发3、NVIC的配置4、完整代码 关于外部中断的AFIO&#xff0c;NVIC的基础知识请参考《stm32标准库入门教程》 链接: link 1、选择外部引脚配置 如上图所示&#xff1a;外部中断配置寄存器AFIO_EXTICR(1~4)中选择EXTI(0 ~ …

开源模型应用落地-模型微调-模型研制-环境准备(一)

一、前言 在自然语言处理&#xff08;NLP&#xff09;的快速发展中&#xff0c;语料采集作为基础性的步骤显得尤为重要。它不仅为机器学习模型提供了所需的训练数据&#xff0c;还直接影响模型的性能和泛化能力。随着数据驱动技术的不断进步&#xff0c;如何有效并高效地收集、…

链式前向星(最通俗易懂的讲解)

链式前向新&#xff1a;用于存储图的 边集 数组 前言 当我们存储图的时候&#xff0c;往往会使用 邻接矩阵 或是 邻接表。 邻接矩阵 好写&#xff0c;但太浪费空间&#xff0c;节点一多就存不下&#xff1b; 邻接表 效率高&#xff0c;但涉及指 &#xff0c;不好写容易出错…

大语言模型入门(三)——提示词编写注意事项

一、提示词编写原则 提示词的编写应当遵循两个原则&#xff0c; 一个是指令必须清晰且具体&#xff0c;另一个是应当给模型充足的时间去思考。首先&#xff0c;你的指令足够清晰和具体&#xff0c;才能让大模型明确你需要它执行的任务&#xff0c;从而降低我们得到无关或者不正…

C# 入坑JAVA 潜规则 注解 列表 listMch,该列表存储了一个映射(Map)的集合 等 入门系列3

java注解 好像和C# 特性 差不多 Data Builder NoArgsConstructor AllArgsConstructor 在Java中&#xff0c;Data、Builder、NoArgsConstructor和AllArgsConstructor是Lombok库提供的注解&#xff0c;它们用于简化Java对象的创建和处理。Lombok是一个流行的Java库&#xff0c;…

java部分总结

一、Java语言发展简史 Java 语言源于 1991 年 4 月&#xff0c; Sun 公司 James Gosling 博士 领导的绿色计划 (Green Project) 开始启动&#xff0c;此计划最 初的目标是开发一种能够在各种消费性电子产品( 如机顶盒、冰箱、收音机等 ) 上运行的程序架构。这个就是 Jav…

基于Pulid-Flux一致性换脸

Pulid-Flux 简介 Pulid-Flux是风格一致性保持和迁移组件&#xff0c;是字节Pulid团队继SDXL的Pulid版本沉淀基础上再次出发布的PuLID-FLUX-v0.9.0 Flux版本风格一致性组件。能够提供了一个无需调整的身份ID一致性和定制化解决方案&#xff0c;能够被应用于风格一致性保持领域包…

如何写出更牛的验证激励

前言 芯片验证是为了发现芯片中的错误而执行的过程&#xff0c;它是一个破坏性的过程。完备的验证激励可以更有效地发现芯片错误&#xff0c;进而缩短验证周期。合格的验证激励必须能产生所有可能的验证场景(完备性)&#xff0c;包括合法和非法的场景&#xff0c;并保持最大的…

Stable Diffusion绘画 | 来训练属于自己的模型:炼丹启动

经过前面几轮辛苦的准备工作之后&#xff0c;现在开始进入终篇的炼丹环节。 在「上传素材」页面&#xff0c;点击「开始训练」&#xff1a; 可以在「查看进度-进度」中&#xff0c;查看模型训练的整体进度&#xff1a; 求助&#xff01;&#xff01;&#xff01;操作「开始训练…

date:10.4(Content:Mr.Peng)( C language practice)

void reverse(char* p, int len) {char* left p;char* right p len - 2;while (left < right){char* temp left;*left *right;//当*left*right后&#xff0c;*temp已经被改为f了*right *temp;//你再*temp赋值给*right时&#xff0c;已经没用了left;right--;}}int main…

前端学习第三天笔记 JavaScript JavaScript的引入 数据类型 运算符 条件语句 字符串

这里写自定义目录标题 JavaScriptJavaScript引入到文件嵌入到HTML文件中引入本地独立js文件引入网络来源文件 JavaScript的注释方式嵌入在HTML文件中的注释JavaScript的输出方式数据类型原始类型&#xff08;基础类型&#xff09;合成类型&#xff08;复合类型&#xff09; 运算…

Github优质项目推荐-第二期

文章目录 Github优质项目推荐 - 第二期一、【hello-algo】&#xff0c;96.1k stars - 算法与数据结构动画图解二、【tabby】&#xff0c;58.6k stars - ssh工具三、【mem0】&#xff0c;22.1k stars - 大模型长期记忆四、【HivisionIDPhotos】&#xff0c;10.6k stars - AI证件…

多智能体协作强化学习中的知识共享

本文提出了一种名为谨慎乐观知识共享&#xff08;CONS&#xff09;的新方法&#xff0c;用于解决合作多智能体强化学习&#xff08;MARL&#xff09;中的知识共享问题。针对传统的行动建议方法可能导致团队探索受阻的情况&#xff0c;即经验丰富的智能体会分享其知识而较不成熟…

【C++算法】10.滑动窗口_长度最小的子数组

文章目录 题目链接&#xff1a;题目描述&#xff1a;解法C 算法代码&#xff1a;图解 题目链接&#xff1a; 209. 长度最小的子数组 题目描述&#xff1a; 解法 解法一&#xff1a;暴力求解&#xff08;会超时&#xff09; 暴力枚举出所有子数组的和。 查找子数组n2&#xff0…

03:(寄存器开发)OLED的简单使用

OLED的简单使用 将江科大的标准库开发OLED的代码进行移植&#xff0c;修改的部分代码如下&#xff1a; /*引脚配置*/ //#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x)) //#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))…