Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 结合源码分析 多线程CAS比较并交换 初识

news2024/11/15 19:43:03

在这里插入图片描述

前言

List、Set、HashMap作为Java中常用的集合,需要深入认识其原理和特性。

本篇博客介绍常见的关于Java中线程安全的ConcurrentHashMap集合的面试问题,结合源码分析题目背后的知识点。

关于List的博客文章如下:

  • Java进阶(List)——面试时List常见问题解读 & 结合源码分析

关于的Set的博客文章如下:

  • Java进阶(Set)——面试时Set常见问题解读 & 结合源码分析

关于HaseMap的博客文章如下:

  • Java进阶(HashMap)——面试时HashMap常见问题解读 & 结合源码分析

其他关于 数据结构 以及 多线程 的文章如下:

  • 数据结构与算法(Data Structures and Algorithm)——跟着Mark Allen Weiss用Java语言学习数据结构与算法
  • 【合集】Java进阶——Java深入学习的笔记汇总 & 再论面向对象、数据结构和算法、JVM底层、多线程、类加载 …

目录

  • 前言
  • 引出
  • ConcurentHashMap 在JDK1.7 和JDK1.8的区别?ConcurentHashMap 是怎么保证线程安全的?
    • 1.什么时候初始化
    • 2.如何保证初始化安全 sizeCtl
    • 3.putval方法存入元素,加锁
  • ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?
  • JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?
  • 涉及到的多线程知识
    • CAS是啥?
    • 比较并交换是啥?
    • CAS的优缺点
    • 什么是CAS机制compareAndSwapInt
  • 总结

引出


1.从结构上来说jdk7中,ConcurentHashMap是采用Segment段数组 + Entry数组 + 单链表结构;
2.从结构上来说jdk8中,ConcurentHashMap 与 HashMap的结构是一模一样的
3.ConcurrentHashMap 不支持 key 或者 value 为 null ,避免歧义;
4.JDK1.7和1.8的区别:

  1. 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  2. 保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+sizeCtl+synchronized保证线程安全。
  3. 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
  4. 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量到达 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
  5. 查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。

5.CAS是一种乐观锁机制,也被称为无锁机制

核心:线程安全

ConcurentHashMap 在JDK1.7 和JDK1.8的区别?ConcurentHashMap 是怎么保证线程安全的?

  1. 从结构上来说jdk7中,ConcurentHashMap是采用Segment段数组 + Entry数组 + 单链表结构

  2. 从结构上来说jdk8中,ConcurentHashMap 与 HashMap的结构是一模一样的

  3. 从线程安全来说,jdk7中,ConcurentHashMap 内部维护的是一个Segment(段)数组,Segment数组中每个元素又是一个HashEntry数组

    在这里插入图片描述

    Segment继承了ReentrantLock这个类,所以segment自然就可以扮演锁的角色,每一个segment相当于一把锁,这就是分段锁

    当其他线程在需要进行put操作时,需要先去获取该对象的锁资源,然而当发现锁资源被占用的时候,该线程会先去进行节点的创建避免线程的空闲,这种思想也叫作预创建的思想

    因为segment在初始化后是不会扩容的,HashEntry数组是会扩容的,与HashMap机制一样,所以HashEntry是依靠于segment锁来维护安全,所以HashEntry的扩容也是线程安全的

1.什么时候初始化

jdk8中,ConcurentHashMap 因为结构变为了 数组+链表+红黑树 结构,所以维护线程安全的机制页相对发生了一些变化,和HashMap同样的,ConcurentHashMap 在put第一个元素时,才会执行初始化

在这里插入图片描述

final V putVal(K key, V value, boolean onlyIfAbsent) {
     if (key == null || value == null) throw new NullPointerException();
     int hash = spread(key.hashCode()); //根据KEY计算hash值
     int binCount = 0;
     for (Node<K,V>[] tab = table;;) {
         Node<K,V> f; int n, i, fh;
         if (tab == null || (n = tab.length) == 0)
             tab = initTable(); //如果数组为null时,表示第一次put,则先进行初始化
         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
             if (casTabAt(tab, i, null,
                          new Node<K,V>(hash, key, value, null)))
                 break;                   // no lock when adding to empty bin
         }
         ...

2.如何保证初始化安全 sizeCtl

在这里插入图片描述

此处核心为sizeCtl

  • sizeCtl的不同值表示不同的含义
  • sizeCtl = 0 代表数组还未初始化
  • sizeCtl > 0 如果数组已经初始化,那么表示 扩容阈值
  • sizeCtl = -1 表示数组正在初始化
  • sizeCtl < -1 表示数组正在扩容,并且正在被多线程初始化中

sizeCtl 这个值,就保证了多线程并发状态下,数组的初始化安全

在这里插入图片描述

核心为compareAndSwapInt,比较主存中数据和当前内存中数据是否相同,如果不同代表有别的线程正在操作这个数据那么就返回false,退回重新争取时间片,此处就是保证并发时线程安全的核心。

在这里插入图片描述

private final Node<K,V>[] initTable() {
     Node<K,V>[] tab; int sc;
 //循环判断数组是否为空/null
     while ((tab = table) == null || tab.length == 0) {
         //此处核心为sizeCtl
         /*
             sizeCtl的不同值表示不同的含义
             sizeCtl = 0 代表数组还未初始化
             sizeCtl > 0 如果数组已经初始化,那么表示 扩容阈值
             sizeCtl = -1 表示数组正在初始化
             sizeCtl < -1 表示数组正在扩容,并且正在被多线程初始化中
         */
         if ((sc = sizeCtl) < 0) //此处表示数组正在被某个线程初始化
             Thread.yield(); // lost initialization race; just spin //释放CPU时间片
         //核心为compareAndSwapInt,比较主存中数据和当前内存中数据是否相同,如果不同代表有别的线程正在操作这个数据那么就返回false,退回重新争取
         //时间片
         //此处就是保证并发时线程安全的核心
         else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { 
             try {
                 if ((tab = table) == null || tab.length == 0) {
                     int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //得到了数组长度是否为自己设置还是默认16
                     @SuppressWarnings("unchecked")
                     Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //new出数组
                     table = tab = nt;//数组赋值
                     sc = n - (n >>> 2);
                 }
             } finally {
                 sizeCtl = sc;  
             }
             break;
         }
     }
     return tab;
 }

CAS + sizeCtl 保证了初始化数据的安全

3.putval方法存入元素,加锁

数组的初始完成后,回到putval方法存入元素

在这里插入图片描述

真正存入元素时,是加入了synchronized来加锁保证线程安全

CAS + sizeCtl 和 synchronized 两者共同保证了ConcurentHashMap 的线程安全!

ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?

  1. 假设 ConcurrentHashMap 允许存放值为 null 的 value,这时有A、B两个线程,线程A调用ConcurrentHashMap.get(key)方法,返回为 null ,我们不知道这个 null 是没有映射的 null ,还是存的值就是 null 。
  2. 假设此时,返回为 null 的真实情况是没有找到对应的 key。那么,我们可以用 ConcurrentHashMap.containsKey(key)来验证我们的假设是否成立,我们期望的结果是返回 false 。
  3. 但是在我们调用 ConcurrentHashMap.get(key)方法之后,containsKey方法之前,线程B执行了ConcurrentHashMap.put(key, null)的操作。那么我们调用containsKey方法返回的就是 true 了,这就与我们的假设的真实情况不符合了,这就有了二义性。

JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?

  1. 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  2. 保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+sizeCtl+synchronized保证线程安全。
  3. 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
  4. 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量到达 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
  5. 查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。

涉及到的多线程知识

CAS是啥?

CAS(CompareAnd Swap),就是比较并交换,是解决多线程情况下,解决使用锁造成性能损耗问题的一种机制。

CAS是一种乐观锁机制,也被称为无锁机制。全称: Compare-And-Swap。它是并发编程中的一种原子操作,通常用于多线程环境下实现同步和线程安全。CAS操作通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果相等,则执行交换操作,否则不执行。由于CAS是一种无锁机制,因此它避免了使用传统锁所带来的性能开销和死锁问题,提高了程序的并发性能。

CAS包含三个操作数:

  • 变量内存位置(V)
  • 预期的变量原值(A)
  • 变量的新值(B)

当要对变量进行修改时,先会将内存位置的值与预期的变量原值进行比较,如果一致则将内存位置更新为新值,否则不做操作,无论哪种情况都会返回内存位置当前的值。

比较并交换是啥?

在这里插入图片描述

package com.tianju.test2;

import java.util.concurrent.atomic.AtomicInteger;

public class CASTest {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,2019));
        System.out.println(atomicInteger);
        System.out.println(atomicInteger.compareAndSet(5,2020));
        System.out.println(atomicInteger.compareAndSet(2019,2020));
        System.out.println(atomicInteger);
        System.out.println(atomicInteger.compareAndSet(2020,5));
        System.out.println(atomicInteger);
    }
}

CAS的优缺点

优点:

CAS是一种无锁机制,因此它避免了使用传统锁所带来的性能开销和死锁问题,提高了程序的并发性能。

缺点:

CAS 有自旋锁,如果不成功会一直循环,可能会给 cpu 带来很大开销;

问题就是可能会造成 “ABA”;

解决方案:解决的思路就是引入类似乐观锁的版本号控制,不止比较预期值和内存位置的值,还要比较版本号是否正确。

在这里插入图片描述

AtomicStampedReference atomicStampedReference = new AtomicStampedReference(5, 1);

在这里插入图片描述

package com.tianju.test2;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CASTest {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,2019));
        System.out.println(atomicInteger);
        System.out.println(atomicInteger.compareAndSet(5,2020));
        System.out.println(atomicInteger.compareAndSet(2019,2020));
        System.out.println(atomicInteger);
        System.out.println(atomicInteger.compareAndSet(2020,5));
        System.out.println(atomicInteger);

        System.out.println(" ########################################## ");
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(5, 1);

        boolean flag1 = atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), 2019,
                atomicStampedReference.getStamp(), 2);
        System.out.println("从5修改为2019:"+flag1);
        System.out.println(atomicStampedReference.getReference());

        boolean flag2 = atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), 2020,
                atomicStampedReference.getStamp(), 3);
        System.out.println("从2019修改为2020:"+flag2);
        System.out.println(atomicStampedReference.getReference());

        boolean flag3 = atomicStampedReference.compareAndSet(2020, 5, 3, 4);
        System.out.println("从2020修改回5:"+flag3);
        System.out.println(atomicStampedReference.getReference());


    }
}

什么是CAS机制compareAndSwapInt

CAS是Java中Unsafe类里面的方法,它的全称是CompareAndSwap,比较并交换的意思。它的主要功能是能够保证在多线程环境下,对于共享变量的修改的原子性。

如下,成员变量state,默认值是0,定义了一个方法doSomething(),这个方法的逻辑是判断state是否为0,如果为0就修改成1。这个逻辑看起来没有任何问题,但是在多线程环境下,会存在原子性的问题,因为这里是一个典型的,Read-Write的操作。

一般情况下,我们会在doSomething()这个方法上加同步锁来解决原子性问题。

在这里插入图片描述

package com.tianju.test2;

public class Demo1 {
    private int state = 0;
    public void doSomething(){
        if (state==0){
            state=1;
        }
    }
}

但是,加同步锁,会带来性能上的损耗,所以,对于这类场景,我们就可以使用CAS机制来进行优化

在这里插入图片描述

package com.tianju.test2;


import sun.misc.Unsafe;

public class Demo2 {
    private volatile int state = 0;
    private static final Unsafe UNSAFE = Unsafe.getUnsafe();
    private static final long stateOffset;

    static {
        try {
            stateOffset = UNSAFE.objectFieldOffset(
                    Demo2.class.getDeclaredField("state")
            );
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

    public void doSomething(){
        if (UNSAFE.compareAndSwapInt(this,stateOffset,0,1)){
            state=1;
        }
    }
}

在doSomething()方法中,我们调用了unsafe类中的compareAndSwapInt()方法来达到同样的目的,这个方法有四个参数,分别是:
当前对象实例、成员变量state在内存地址中的偏移量、预期值0、期望更改之后的值1。

CAS机制会比较state内存地址偏移量对应的值和传入的预期值0是否相等,如果相等,就直接修改内存地址中state的值为1。否则,返回false,表示修改失败,而这个过程是原子的,不会存在线程安全问题。

CompareAndSwap是一个native方法,实际上它最终还是会面临同样的问题,就是先从内存地址中读取state的值,然后去比较,最后再修改。

这个过程不管是在什么层面上实现,都会存在原子性问题。所以,CompareAndSwap的底层实现中,在多核CPU环境下,会增加一个Lock指令对缓存或者总线加锁,从而保证比较并替换这两个指令的原子性。

CAS主要用在并发场景中,比较典型的使用场景有两个:

  1. 第一个是J.U.C里面Atomic的原子实现,比如AtomicInteger,AtomicLong。
  2. 第二个是实现多线程对共享资源竞争的互斥性质,比如在AQS、ConcurrentHashMap、ConcurrentLinkedQueue等都有用到。

总结

1.从结构上来说jdk7中,ConcurentHashMap是采用Segment段数组 + Entry数组 + 单链表结构;
2.从结构上来说jdk8中,ConcurentHashMap 与 HashMap的结构是一模一样的
3.ConcurrentHashMap 不支持 key 或者 value 为 null ,避免歧义;
4.JDK1.7和1.8的区别:

  1. 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  2. 保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+sizeCtl+synchronized保证线程安全。
  3. 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
  4. 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量到达 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
  5. 查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。

5.CAS是一种乐观锁机制,也被称为无锁机制

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

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

相关文章

IDEA MyBatisX插件介绍

一、前言 前几年写代码的时候&#xff0c;要一键生成DAO、XML、Entity基础代码会采用第三方工具&#xff0c;比如mybatis-generator-gui等&#xff0c;现在IDEA或Eclipse都有对应的插件&#xff0c;像IDEA中MyBatisX就是一个比较好用的插件。 二、MyBatisX安装配置使用 MyBa…

C的缺陷和陷阱读书笔记

词法陷阱 1、if语句的特殊用法 1、if(x>max) maxx;2、if(x>max?x;max) //条件表达式&#xff0c;是执行第二个&#xff0c;否执行第三个3、if(x>max); //条件成立后执行——空语句4、if((fopen(arg v[i],0))>0) //open函数执行&#xff0c;成功返回后面的0&a…

Maven系列第9篇:多环境构建,作为核心开发,这个玩不转有点说不过去!

如果你作为公司核心开发&#xff0c;打算使用maven来搭建项目骨架&#xff0c;这篇文章的内容是你必须要掌握的。 平时我们在开发系统的时候&#xff0c;会有开发环境、测试环境、线上环境&#xff0c;每个环境中配置文件可能都是不一样的&#xff0c;比如&#xff1a;数据库的…

Matlab2022b图文安装保姆级教程

注意&#xff1a;完成安装步骤1和步骤2之后&#xff0c;再去使用Matlab2022b 本次安装后的版本信息如下&#xff0c;64位软件&#xff0c;windows系统 Matlab2022a与2022b的比较 MATLAB主要用于数据分析、无线通信、深度学习、图像处理与计算机视觉、信号处理、量化金融与风险…

宝塔Linux面板Java项目前后端部署 (PHP部署前端文件)

1. 上传前端文件 将整个文件夹拖进来 2. PHP项目 (添加站点) 添加证书SSL 新增配置文件 location /dev-api/{proxy_set_header Host $http_host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header REMOTE-HOST $remote_addr;proxy_set_header X-Forwarded-For $proxy_…

Multi-gpu问题(1)

Multi-gpu问题 域在z方向划分为num_gpus段&#xff0c;其中num_gpus表示可用GPU的数量&#xff0c;然后每个GPU相应地负责一个大小为nxny&#xff08;nz/num_gpus&#xff09;的子域。 虽然整个域在主机端表示&#xff0c;但gpu只存储它们的子域。由于更新GPU k&#xff08;k …

【计算机网络笔记】传输层——UDP简介

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

安防监控项目---web点灯(网页发送命令控制A9的led)

文章目录 前言一、web点亮LED流程二、静态网页设计&#xff08;html界面&#xff09;三、 CGI和BOA在本项目中的使用总结 前言 书接上期&#xff0c;和大家分享的是web点灯&#xff0c;哈哈哈&#xff0c;谈论起点灯这个词&#xff0c;这么久以来我已然已经成长为一名合格的点…

C#,数值计算——分类与推理Svmpolykernel的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { public class Svmpolykernel : Svmgenkernel { public int n { get; set; } public double a { get; set; } public double b { get; set; } public double d { get; set; …

gRPC源码剖析-Builder模式

一、Builder模式 1、定义 将一个复杂对象的构建与表示分离&#xff0c;使得同样的构建过程可以创建不同的的表示。 2、适用场景 当创建复杂对象的算法应独立于该对象的组成部分以及它们的装配方式时。 当构造过程必须允许被构造的对象有不同的表示时。 说人话&#xff1a…

java之数组的定义以及使用

文章目录 定义数组1. 定义数组并分配内存空间&#xff1a;2. 定义并初始化数组的值&#xff1a;3. 动态初始化数组&#xff1a;4. 使用数组长度属性&#xff1a;5. 多维数组的定义&#xff1a; 数组的应用数组赋值&#xff1a;1. 直接赋值&#xff1a;直接初始化数组&#xff1…

多线程---synchronized特性+原理

文章目录 synchronized特性synchronized原理锁升级/锁膨胀锁消除锁粗化 synchronized特性 互斥 当某个线程执行到某个对象的synchronized中时&#xff0c;其他线程如果也执行到同一个对象的synchronized就会阻塞等待。 进入synchronized修饰的代码块相当于加锁 退出synchronize…

【UE 模型描边】UE5中给模型描边 数字孪生 智慧城市领域 提供资源下载

目录 0 引言1 Soft Outlines1.1 虚幻商城1.2 使用步骤 2 Auto Mesh Outlines2.1 虚幻商城2.2 使用步骤 3 Survivor Vision3.1 虚幻商城3.2 使用步骤 结尾 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;UE虚幻引擎专栏&#x1f4a5; 标题&#xf…

设计模式_状态模式

状态模式 介绍 设计模式定义案例问题堆积在哪里解决办法状态模式一个对象 状态可以发生改变 不同的状态又有不同的行为逻辑游戏角色 加载不同的技能 每个技能有不同的&#xff1a;攻击逻辑 攻击范围 动作等等1 状态很多 2 每个状态有自己的属性和逻辑每种状态单独写一个类 角色…

一个注解,实现数据脱敏-plus版

shigen坚持日更的博客写手&#xff0c;擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。坚持记录和分享从业两年以来的技术积累和思考&#xff0c;不断沉淀和成长。 当看到这个文章名的时候&#xff0c;是不是很熟悉&#xff0c;是的shigen之前发表了一个这…

[C++]命名空间等——喵喵要吃C嘎嘎

希望你开心&#xff0c;希望你健康&#xff0c;希望你幸福&#xff0c;希望你点赞&#xff01; 最后的最后&#xff0c;关注喵&#xff0c;关注喵&#xff0c;关注喵&#xff0c;大大会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的…

CSRF 篇

一、CSRF 漏洞&#xff1a; 1、漏洞概述&#xff1a; &#xff08;1&#xff09;一般情景&#xff1a; 利用已认证用户的身份执行未经用户授权的操作。攻击者试图欺骗用户在其不知情的情况下执行某些操作&#xff0c;通常是在受害者已经登录到特定网站的情况下。 &#xff0…

《动手深度学习》线性回归简洁实现实例

&#x1f388; 作者&#xff1a;Linux猿 &#x1f388; 简介&#xff1a;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;Linux、C/C、云计算、物联网、面试、刷题、算法尽管咨询我&#xff0c;关注我&#xff0c;有问题私聊&#xff01; &…

百度富文本上传图片后样式崩塌

&#x1f525;博客主页&#xff1a; 破浪前进 &#x1f516;系列专栏&#xff1a; Vue、React、PHP ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 问题描述&#xff1a;上传图片后&#xff0c;图片会变得很大&#xff0c;当点击的时候更是会顶开整个的容器的高跟宽 原因&#…

【3D 图像分割】基于 Pytorch 的 VNet 3D 图像分割7(数据预处理)

在上一节&#xff1a;【3D 图像分割】基于 Pytorch 的 VNet 3D 图像分割6&#xff08;数据预处理&#xff09; 中&#xff0c;我们已经得到了与mhd图像同seriesUID名称的mask nrrd数据文件了&#xff0c;可以说是一一对应了。 并且&#xff0c;mask的文件&#xff0c;还根据结…