线程池的使用、工作原理和优势

news2024/11/16 19:56:36

关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。
专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。

目录

  • 一、导读
  • 二、线程池概览
    • 2.1 为什么创建和销毁线程开销较大
    • 2.2 为什么要使用线程池?
    • 2.3 在配置线程池的时候需要考虑哪些配置因素?
  • 三、线程池使用
    • 3.1 线程池的创建
      • 3.1.1 newFixedThreadPool
      • 3.1.2 newCachedThreadPool
      • 3.1.3 newScheduledThreadPool
      • 3.1.4 newSingleThreadExecutor
    • 3.2 ThreadPoolExecutor
    • 3.3 为什么线程池不推荐使用Executors去创建,而是通过ThreadPoolExecutor的方式
  • 四、线程池原理
    • 4.1 线程池中常用的workQueue(缓冲队列)
    • 4.2 线程池中拒绝策略
    • 4.3 线程池任务的关闭
    • 4.4 线程的复用
  • 五、 推荐阅读

ddd

一、导读

我们继续总结学习Java基础知识,温故知新。

线程池。

二、线程池概览

在Java中,创建和销毁线程开销较大,为了避免线程过多而带来使用上的开销。
所以我们需要对线程进行统一管理及复用,这就是我们要说的线程池。

线程池用于管理和复用多个线程,把一个或多个线程通过统一的方式进行调度和重复使用的技术。

从JDK 5开始,把工作单元与执行机制分离开来,工作单元包括Runnable和Callable,而执行机制由Executor框架提供。

2.1 为什么创建和销毁线程开销较大

创建和销毁线程的开销较大主要是因为涉及到以下几个方面:

  1. 上下文切换:在多线程环境中,当一个线程被创建或销毁时,操作系统需要切换上下文,将CPU的执行权从一个线程转移到另一个线程。这个过程涉及保存和恢复线程的状态信息,包括寄存器值、栈指针和程序计数器等。上下文切换是一项耗时的操作,会导致额外的开销。

  2. 内存管理:每个线程需要分配一定的内存空间来存储线程的堆栈、线程私有数据等。创建和销毁线程会涉及内存的分配和释放,而内存分配和释放操作通常比较耗时。

  3. 调度开销:操作系统需要进行调度,决定哪个线程应该获得CPU的执行权。线程的创建和销毁会引起调度器的重新调度,这涉及到时间片、优先级和调度算法等方面的开销。

  4. 同步和通信:多线程编程中,线程之间通常需要进行同步和通信,以确保数据的一致性和线程间的协调。创建和销毁线程会涉及到锁、信号量、管道等同步和通信机制的初始化和清理,增加了开销。

2.2 为什么要使用线程池?

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。

2.3 在配置线程池的时候需要考虑哪些配置因素?

从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。并且近可能地使用有界的工作队列。
性质不同的任务可用使用不同规模的线程池分开处理:

  • CPU密集型: 尽可能少的线程,Ncpu+1
  • IO密集型: 尽可能多的线程, Ncpu*2,比如数据库连接池
  • 混合型: CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分

CPU密集型和IO密集型任务的权衡:如何找到最佳平衡点

三、线程池使用

3.1 线程池的创建

java提供了多种方式:

1. newFixedThreadPool(n):创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,
可用于控制程序的最大并发数。
 
2. newCacheThreadPool():短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,
并试图缓存线程以便重复使用,如果限制 60 秒没被使用,则会被移除缓存。如果现有线程没有可用的,
则创建一个新线程并添加到池中,如果有被使用完但是还没销毁的线程,就复用该线程。
终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
 
3. newScheduledThreadPool():创建一个数量固定的线程池,支持执行定时性或周期性任务。
 
4. newWorkStealingPool(n):Java 8 新增创建线程池的方法,创建时如果不设置任何参数,
则以当前机器CPU 处理器数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。
 
5. newSingleThreadExecutor():创建一个单线程的线程池。这个线程池只有一个线程在工作,
也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
 
6. newSingleThreadScheduledExecutor():此线程池就是单线程的 newScheduledThreadPool。
 

3.1.1 newFixedThreadPool

创建固定大小的线程池,比如线程池容量是10,最多可以同时执行10个线程。
使用案例

创建线程池,参数是创造的线程数量
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
   int j = i;
   pool.execute(new Runnable() {
       @Override
       public void run() {
       }
   });
}

3.1.2 newCachedThreadPool

创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于JVM能够创建的最大线程大小,当然线程池里的线程是可以复用的,但是如果在高并发的情况下,这个线程池在会导致运行时内存溢出问题。
使用案例

ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 6; i++) {
   int j = i;
   executorService.execute(new Runnable() {
       @Override
       public void run() {
       }
   });
}

3.1.3 newScheduledThreadPool

创建一个定时执行的线程池,里边提供了两个方法,FixRate和fixDelay,
fixRate 就是以固定时间周期执行任务,不管上一个线程是否执行完,
fixDelay 的话就是以固定的延迟执行任务,就是在上一个任务执行完成之后,延迟一定时间执行。
使用案例

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
for (int i = 0; i < 5; i++) {
   int j = i;
   executorService.schedule(new Runnable() {
       @Override
       public void run() {
       }
   }, 3L, TimeUnit.SECONDS);
}

3.1.4 newSingleThreadExecutor

创建一个单线程的线程池,这个线程池同时只能执行一个线程,可以保证线程按顺序执行,保证数据安全。
使用案例

public class SingleThreadPoolDemo {
    //格式化
    static SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    //AtomicInteger用来计数
    static AtomicInteger number = new AtomicInteger();
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 6; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                }
            });
        }
    }
}

3.2 ThreadPoolExecutor

通过ThreadPoolExecutor的方式创建线程池,前面四种都不是我们推荐都方式

public class ThreadPoolDemo {
    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
      2,   // 核心线程数
      10,  // 最大线程数
      10L, // 线程存活时间
      TimeUnit.SECONDS,  // 线程存活时间单位
      new LinkedBlockingQueue(100));// 缓冲队列
    
    public static void main(String[] args) {
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
            }
        });
    }
}

3.3 为什么线程池不推荐使用Executors去创建,而是通过ThreadPoolExecutor的方式

newFixedThreadPool(固定线程数)
newSingleThreadExecutor(单线程)

  • 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。线程数固定,任务多了之后容易堆积。

newCachedThreadPool(可缓存的线程池)
newScheduledThreadPool(定时执行的线程池)

  • 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。不限定线程多数量,任务一多,容易创建无限多线程。

四、线程池原理

先看个图,方便理解
在这里插入图片描述

  1. 当有新的任务进来时,线程池将当前线程数量核心数量进行比较,如果没有超过核心数就会新建线程进行任务执行,
  2. 如果已经超过核心线程数,则判断缓冲队列是否已经满了,没有满的话任务就会被放入缓冲队列中排队等待执行;
  3. 如果缓冲队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;
  4. 如果超过了最大线程数,就会执行拒绝执行策略。

再简单点,就两个队列,一个线程集合workerSet和一个阻塞队列workQueue
在这里插入图片描述

在这里插入图片描述

我们一起来看看源码,
线程池类为 java.util.concurrent.ThreadPoolExecutor,常用构造方法为:

ThreadPoolExecutor(
   int corePoolSize,   // 核心线程数
   int maximumPoolSize,   // 最大线程数
   long keepAliveTime,     // 线程存活时间
   TimeUnit unit,          // 线程存活时间单位
   BlockingQueue<Runnable> workQueue,  // 缓冲队列
   RejectedExecutionHandler handler    // 拒绝策略
) 

处理任务的优先级为:核心线程 > 缓冲队列 > 最大线程
corePoolSize > workQueue > maximumPoolSize

如果三者都满了,使用handler处理被拒绝的任务。

/**
 * 将该Runnable任务加入线程池并在未来某个时刻执行
 * 该任务可能执行在一个新的线程 或 一个已存在的线程池中的线程
 * 如果该任务提交失败,可能是因为线程池已关闭,或者已达到线程池队列和线程数已满.
 * 该Runnable将交给RejectedExecutionHandler处理,抛出RejectedExecutionException
 */
public void execute(Runnable command) {
    if (command == null){
        //如果没传入Runnable任务,则抛出空指针异常
        throw new NullPointerException();
    }
    
    int c = ctl.get();
    //当前线程数 小于 核心线程数
    if (workerCountOf(c) < corePoolSize) {
        //直接开启新的线程,并将Runnable传入作为第一个要执行的任务,成功返回true,否则返回false
        if (addWorker(command, true)){
            return;
        }
        c = ctl.get();
    }

    //c < SHUTDOWN代表线程池处于RUNNING状态 + 将Runnable添加到任务队列,如果添加成功返回true失败返回false
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        //成功加入队列后,再次检查是否需要添加新线程(因为已存在的线程可能在上次检查后销毁了,或者线程池在进入本方法后关闭了)
        if (! isRunning(recheck) && remove(command)){
            //如果线程池处于非RUNNING状态 并且 将该Runnable从任务队列中移除成功,则拒绝执行此任务
            //交给RejectedExecutionHandler调用rejectedExecution方法,拒绝执行此任务
            reject(command);
        }else if (workerCountOf(recheck) == 0){
            //如果线程池线程数量为0,则创建一条新线程,去执行
            addWorker(null, false);
        }   
    }else if (!addWorker(command, false))
        //如果线程池处于非RUNNING状态 或 将Runnable添加到队列失败(队列已满导致),则执行默认的拒绝策略
        reject(command);
}

当ThreadPoolExecutor创建新线程时,通过CAS来更新线程池的状态ctl.

4.1 线程池中常用的workQueue(缓冲队列)

  1. ArrayBlockingQueue(有界缓存等待队列)
    可以指定缓存队列的大小

  2. LinkedBlockingQueue(无界缓存等待队列)
    可以创建Integer.MAX_VALUE个线程,容易OOM

当前执行的线程数量达到corePoolSize(核心)的数量时,剩余的元素会在阻塞队列里等待,在使用此阻塞队列时maximumPoolSizes就相当于无效了。

  1. SynchronousQueue(无缓冲等待队列)
    是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界,避免线程拒绝执行操作

4.2 线程池中拒绝策略

所有拒绝策略都实现了接口 RejectedExecutionHandler

public interface RejectedExecutionHandler {
    /**
     * @param r  待执行任务
     * @param executor 线程池
     * @throws RejectedExecutionException  方法可能会抛出拒绝异常
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

1.AbortPolicy
直接抛出拒绝异常,会中断调用者的处理过程,所以除非有明确需求,一般不推荐

 public static class AbortPolicy implements RejectedExecutionHandler {
     public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
         throw new RejectedExecutionException("Task " + r.toString() +
                                              " rejected from " +
                                              e.toString());
     }
 }

2.CallerRunsPolicy
在调用者线程中运行当前被丢弃的任务,也就是说谁把 runnable 这个任务甩出来。
用调用者所在线程来运行任务,也就是说任务不会进入线程池。
如果线程池已经被关闭,则直接丢弃该任务

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}
  1. DiscardOledestPolicy
    丢弃队列中最老的,然后再次尝试提交新任务。
 public static class DiscardOldestPolicy implements RejectedExecutionHandler {
     public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
         if (!e.isShutdown()) {
             //获得待执行的任务队列,队列先进先出
             //poll()方法就能直接把队列中最老的抛弃掉,再次尝试执行execute(r)
             e.getQueue().poll();
             e.execute(r);
         }
     }
 }
  1. DiscardPolicy
    默默丢弃无法加载的任务。这个代码就很简单了,真的是啥也没做。
public static class DiscardPolicy implements RejectedExecutionHandler {
  public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
  }
}
  1. 自定义拒绝策略
    只要继承接口都可以根据自己需要自定义拒绝策略.

案例1:
单独启动一个新的临时线程来执行任务。

private class NewThreadRunsPolicy implements RejectedExecutionHandler {
  public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
      try {
          final Thread t = new Thread(r, "Temporary task executor");
          t.start();
      } catch (Throwable e) {
          throw new RejectedExecutionException(
                  "Failed to start a new thread", e);
      }
  }
}

案例2:
直接继承的 AbortPolicy ,加强了日志输出,并且输出dump文件,然后任务也是拒绝

public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        String msg = String.format("Thread pool is EXHAUSTED!" +
                        " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
                        " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
                threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
                e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
                url.getProtocol(), url.getIp(), url.getPort());
        logger.warn(msg);
        dumpJStack();
        throw new RejectedExecutionException(msg);
    }
}

4.3 线程池任务的关闭

shutdown方法会将线程池的状态设置为SHUTDOWN,线程池进入这个状态后,就拒绝再接受任务,然后会将剩余的任务全部执行完

  1. 临时线程什么时候创建?
    新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,
    此时才会创建临时线程;
  2. 什么时候会开始拒绝任务?
    核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务

4.4 线程的复用

在ThreadPoolExecutor.java的runwork方法中通过一个while循环,不断的getTask()取任务出来执行,以这种方式实现了线程的复用.

五、 推荐阅读

Java 专栏

SQL 专栏

数据结构与算法

Android学习专栏

在这里插入图片描述

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

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

相关文章

tinkerCAD案例:26. Making the Amplifier Body 制作放大器主体(1)

tinkerCAD案例&#xff1a;26. Making the Amplifier Body 制作放大器主体 Project Overview: 项目概况&#xff1a; Music is the universal language! And who doesn’t love jamming out to some sweet tunes with friends? But it’s such a bummer when there are more…

【腾讯云Cloud Studio实战训练营】Cloud Studio 快速搭建学习分享

文章目录 零、前言一、Cloud Studio1.1、Cloud Studio是什么1.2、Cloud Studio的优势 二、实战&#xff1a;快速构建React完成点餐H5页面还原2.1、打开官网2.2、选择React 框架模板2.3、编码部分安装 antd-mobile安装 Less安装 normalize上传项目需要的素材替换App.js主文件 2.…

ChatGPT结合知识图谱构建医疗问答应用 (二) - 构建问答流程

一、ChatGPT结合知识图谱 上篇文章对医疗数据集进行了整理&#xff0c;并写入了知识图谱中&#xff0c;本篇文章将结合 ChatGPT 构建基于知识图谱的问答应用。 下面是上篇文章的地址&#xff1a; ChatGPT结合知识图谱构建医疗问答应用 (一) - 构建知识图谱 这里实现问答的流程…

无涯教程-jQuery - Tabs组件函数

窗口小部件选项卡函数可与JqueryUI中的窗口小部件一起使用。选项卡用于在分成逻辑部分的内容之间交换。 Tabs - 语法 $( "#tabs" ).tabs(); Tabs - 示例 以下是显示Tab用法的简单示例- <!doctype html> <html lang"en"><head><m…

选择排序算法

选择排序 算法说明与代码实现&#xff1a; 以下是使用Go语言实现的选择排序算法示例代码&#xff1a; package mainimport "fmt"func selectionSort(arr []int) {n : len(arr)for i : 0; i < n-1; i {minIndex : ifor j : i 1; j < n; j {if arr[j] < a…

一篇关于预测“未来”的教程:运行在 Intel AIxBoard™ 开发板上的 TDengine

英特尔数字化开发套件 AIxBoard 是一款 AI 架构的人工智能嵌入式开发板&#xff0c;体积小巧功能强大&#xff0c;可以在时序数据预测、图像分类、目标检测分割和语音处理等应用中并行运行多个神经网络。作为一款面向专业创客、开发者的功能强大的小型计算机&#xff0c;借助开…

牛客网Verilog刷题——VL48

牛客网Verilog刷题——VL48 题目答案 题目 在data_en为高期间&#xff0c;data_in将保持不变&#xff0c;data_en为高至少保持3个B时钟周期。表明&#xff0c;当data_en为高时&#xff0c;可将数据进行同步。本题中data_in端数据变化频率很低&#xff0c;相邻两个数据间的变化&…

字符串性能优化

String 对象作为 Java 语言中重要的数据类型&#xff0c;是内存中占据空间最大的一个对象。高效地 使用字符串&#xff0c;可以提升系统的整体性能。 来一到题来引出这个话题 通过三种不同的方式创建了三个对象&#xff0c;再依次两两匹配&#xff0c;每组被匹配的两个对象是否…

Eclipse使用Ctrl键导致程序卡死的解决方案

在Eclipse中&#xff0c;经常可以使用Ctrl鼠标单击&#xff0c;可以直接将编辑界面引导到相关的方法&#xff0c;属性&#xff0c;或者类。 这个功能确实非常好用&#xff0c;但是由于复制粘贴的功能快捷键也是Ctrl&#xff0c;以致我在快速进行操作的时候&#xff0c;Eclipse…

tinkerCAD案例:27. Build a Mobile Amplifier 构建移动放大器(2)

tinkerCAD案例&#xff1a;27. Build a Mobile Amplifier 构建移动放大器(2) 原文 step 1 Lesson Overview: 课程概述&#xff1a; Now we’re going to adapt the shape to your device! 现在&#xff0c;我们将根据您的设备调整形状&#xff01; step 2 o create an in…

【雕爷学编程】MicroPython动手做(25)——语音合成与语音识别2

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

浏览器安装selenium IDE插件并进行网页测试记录

Chrome开发者工具插件,谷歌浏览器开发者工具插件推荐下载_安装_教程-扩展迷 去官网直接搜索下载需要的插件就可。 插件下载安装-Chrome-扩展迷 下载好后解压&#xff1a; 打开Chrome谷歌浏览器&#xff1a; 设置>拓展程序>打开"开发者模式”>将下载好的seleni…

【多模态】21、BARON | 通过引入大量 regions 来提升模型开放词汇目标检测能力(CVPR2021)

文章目录 一、背景二、方法2.1 主要过程2.2 Forming Bag of Regions2.3 Representing Bag of Regions2.4 Aligning bag of regions 三、效果 论文&#xff1a;Aligning Bag of Regions for Open-Vocabulary Object Detection 代码&#xff1a;https://github.com/wusize/ovdet…

SciencePub学术 | 人工智能类重点SCIEEI征稿中

SciencePub学术 刊源推荐: 人工智能类重点SCIE&EI征稿中&#xff01;信息如下&#xff0c;录满为止&#xff1a; 一、期刊概况&#xff1a; 人工智能类重点SCIE&EI 【期刊简介】IF&#xff1a;6.5-7.0&#xff0c;JCR1区&#xff0c;中科院2区&#xff1b; 【出版社…

画架构图工具-haydn

Haydn解决方案数字化平台_海顿解决方案工具链-华为云 下图为haydn架构图示例 Haydn解决方案数字化平台_海顿解决方案工具链-华为云 1、vpc是一个很重要的元素&#xff0c;有网络隔离的作用。 2、OBS、CES、CTS&#xff0c;不需要画到vpc里面。 3、不在区域内的资源&#xf…

Panda 编译时原子化 CSS-in-JS 框架的跨平台方案

Panda 编译时原子化 CSS-in-JS 框架的跨平台方案 Panda 编译时原子化 CSS-in-JS 框架的跨平台方案 对编译时原子化CSS框架的思考编译时 CSS-in-JS 方案对比 LinariaPandacss总结 weapp-pandacss 介绍快速开始 pandacss 安装和配置 0. 安装和初始化 pandacss1. 配置 postcss2. …

Hbase pe 压测 OOM问题解决

说明&#xff1a;本人使用CDH虚拟机搭建了Hbase集群&#xff0c;但是在压测的时发现线程多个的时候直接回OOM,记录一下 执行命令 hbase pe --nomapred --oneContrue --tablerw_test_1 --rows1000 --valueSize100 --compressSNAPPY --presplit10 --autoFlushtrue randomWrite …

SDXL 1.0 介绍和优缺点总结

2023年7月26日:Stability. AI 发布SDXL 1.0&#xff0c;这是对其生成模型的又一次重大更新&#xff0c;带来了突破性的变化。 SDXL 1.0包括两种不同的模型: sdxml -base-1.0:生成1024 x 1024图像的基本文本到图像模型。基本模型使用OpenCLIP-ViT/G和CLIP-ViT/L进行文本编码。…

详解c++继承与多继承

目录 &#x1f684;什么是继承&#x1f689;继承的概念&#x1f683;继承的定义 &#x1f687;继承基类成员访问方式的变化&#x1f686;基类和派生类对象赋值转换&#x1f690;继承时的作用域&#x1f697;派生类的默认成员函数&#x1f693;继承、友元、静态成员&#x1f69a…

运维级影像归档与通信系统(PACS)源码

运维级医院PACS系统源码&#xff0c;带演示&#xff0c;带使用手册和操作说明书 &#xff0c;带三维重建与还原功能&#xff0c;开发环境&#xff1a;VC MSSQL。 一、影像归档与通信系统&#xff08;PACS&#xff09;概述 PACS影像归档与通信系统”( Picture Archiving and C…