《CUDA编程》8.共享内存的合理使用

共享内存是 一种可被程序员直接操控的缓存,主要作用有两个:
①减少核函数中对全局内存的访 问次数,实现高效的线程块内部的通信
②提高全局内存访问的合并度
将通过两个具体的例子阐明共享内存的合理使用,一个数组归约的例子和讨矩阵转置的例子

1 例子:数组归约计算

一个有 N N N 个元素的数组 x x x。假如我们需要计算该数组中所有元素的和,即 s u m = x [ 0 ] + x [ 1 ] + . . . + x [ N − 1 ] sum = x[0] + x[1] + ... + x[N - 1] sum=x[0]+x[1]+...+x[N1],这里先给出C++的代码:

float cumulative_sum(const float* x, int N) {float sum = 0.0;for (int i = 0; i < N; i++) {sum += x[i];}return sum;
}

在这个例子中,我们考虑一个长度为 1 0 8 10^{8} 108 的一维数组,在主函数中,我们将每个数组元素初始化为 1.23,调用函数 reduce 并计时。

  • 在使用双精度浮点数时,输出:sum = 123000000.110771,该结果前 9 位有效数字都正确,从第 10位开始有错误,运行速度为315ms。
  • 在使用单精度浮点数时,输出:sum = 33554432.000000.,该结果完全错误,运行速度为315ms。

这是因为,在累加计算中出现了所谓的"大数吃小数"的现象。单精度浮点数只有 6、7 位精确的有效数字。在上面的函数中,将变量 sum 的值累加到 3000多万后,再将它和1.23相加,其值就不再增加了(小数被大数"吃掉了",但大数并没有变化)。
当然现在已经有了其他安全的算法,但我们在CUDA 实现要比上述 C++ 实现稳健(robust)得多,使用单精度浮点数时 结果也相当准确。

1.1 只使用全局内存

1.1.1 运行代码

#include <cuda_runtime.h>
#include <iostream>
#include <iomanip> 
#include "error_check.cuh"#define TILE_DIM 32  // 定义每个block的线程块维度// 核函数 
__global__ void Array_sum(float* d_x, float* d_re)
{const int tid = threadIdx.x;float* x = d_x + blockIdx.x * blockDim.x; // 使用 blockIdx.x 来索引数据// 归约求和for (int stride = blockDim.x >> 1; stride > 0; stride >>= 1){if (tid < stride){x[tid] += x[tid + stride]; // 将数据进行归约}__syncthreads(); // 同步线程,确保所有线程完成了归约计算}// 只在第一个线程中写入结果if (tid == 0){d_re[blockIdx.x] = x[0];}
}int main() {// 定义一维数组大小const int N = 100000000;const int size = N * sizeof(float);// 主机上分配内存float* h_A = (float*)malloc(size);const int gridSize = (N + TILE_DIM - 1) / TILE_DIM; // 计算网格数量float* h_re = (float*)malloc(gridSize * sizeof(float)); // 结果数组的大小// 初始化数组数据都为1.23for (int i = 0; i < N; i++){h_A[i] = 1.23f;}// 在设备上分配内存float* d_A, * d_re;CHECK(cudaMalloc((void**)&d_A, size));CHECK(cudaMalloc((void**)&d_re, gridSize * sizeof(float))); // 修正结果数组的大小// 将主机数组数据拷贝到设备CHECK(cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice));// 创建线程块和线程块网格const int blockSize = TILE_DIM; // 每个线程块的线程数量dim3 threads(blockSize); // 一维线程块dim3 gridSize2(gridSize); // 一维网格// 实现计时cudaEvent_t start, stop;CHECK(cudaEventCreate(&start));CHECK(cudaEventCreate(&stop));CHECK(cudaEventRecord(start));// 调用核函数,进行数组归约Array_sum << <gridSize2, threads >> > (d_A, d_re);CHECK(cudaEventRecord(stop));CHECK(cudaEventSynchronize(stop));float milliseconds = 0;CHECK(cudaEventElapsedTime(&milliseconds, start, stop));// 输出运行时间,单位是msstd::cout << "运行时间:" << milliseconds << "ms" << std::endl;// 将结果 re 从设备拷贝到主机CHECK(cudaMemcpy(h_re, d_re, gridSize * sizeof(float), cudaMemcpyDeviceToHost));// 计算最终结果double total_sum = 0.0;for (int i = 0; i < gridSize; i++) {total_sum += h_re[i];}// 输出结果,精度为小数点后10位std::cout << std::fixed << std::setprecision(6); // 你可以根据需要调整精度std::cout << "最终结果:" << total_sum << std::endl;// 释放主机和设备内存free(h_A);free(h_re);CHECK(cudaFree(d_A));CHECK(cudaFree(d_re));return 0;
}

输出结果
在这里插入图片描述
观察结果发现,在CUDA中使用单精度进行计算,不仅运算结果正确,而且速度也比c++代码快了5倍(我的设备是GTX 1650)

1.1.2 分析代码

__syncthreads()函数
在核函数中:

for (int stride = blockDim.x >> 1; stride > 0; stride >>= 1){if (tid < stride){x[tid] += x[tid + stride]; // 将数据进行归约}__syncthreads(); // 同步线程,确保所有线程完成了归约计算}

在归约操作后面使用了__syncthreads()函数,是因为核函数操作是多线程计算的,所以可能上一个归约操作还没有完成,下一个归约操作就开启了,可能导致计算错误,为了保证顺序进行,所以使用该函数。

②位运算

for (int stride = blockDim.x >> 1; stride > 0; stride >>= 1){}

这里使用“右移一位”来替代“除以2”的操作,因为位运算的速度更快

1.2 使用共享内存

全局内存的访问速度是所有内存中最低的,应该尽量减少对它的使用。寄 存器是最高效的,但在需要线程合作的问题中,用仅对单个线程可见的寄存器是不够的。

所以共享内存成为最佳选择,因为它提供了一个全局可见、快速且高效的存储空间,供同一个线程块内的所有线程使用。

在核函数中,要将一个变量定义为共享内存变量,就要在定义语句中加上一个限定 符__shared__。一般情况下,我们需要的是一个长度等于线程块大小的数组。

1.2.1 修改代码

修改了核函数代码和TILE_DIM 的值,保证block的大小和共享内存的大小一致,其余不变

#include <cuda_runtime.h>
#include <iostream>
#include <iomanip> 
#include "error_check.cuh"#define TILE_DIM 128  // 定义每个block的线程块维度// 核函数 
__global__ void Array_sum(float* d_x, float* d_re)
{const int tid = threadIdx.x;const int bid = blockIdx.x;const int n   = bid * blockDim.x + tid;__shared__ float s_y[TILE_DIM];s_y[tid] = (n < 100000000) ? d_x[n] : 0.0;__syncthreads();// 归约求和for (int stride = blockDim.x >> 1; stride > 0; stride >>= 1){if (tid < stride){s_y[tid] += s_y[tid + stride]; // 将数据进行归约}__syncthreads(); // 同步线程,确保所有线程完成了归约计算}// 只在第一个线程中写入结果if (tid == 0){d_re[blockIdx.x] = s_y[0];}
}int main() {// 定义一维数组大小const int N = 100000000;const int size = N * sizeof(float);// 主机上分配内存float* h_A = (float*)malloc(size);const int gridSize = (N + TILE_DIM - 1) / TILE_DIM; // 计算网格数量float* h_re = (float*)malloc(gridSize * sizeof(float)); // 结果数组的大小// 初始化数组数据都为1.23for (int i = 0; i < N; i++){h_A[i] = 1.23f;}// 在设备上分配内存float* d_A, * d_re;CHECK(cudaMalloc((void**)&d_A, size));CHECK(cudaMalloc((void**)&d_re, gridSize * sizeof(float))); // 修正结果数组的大小// 将主机数组数据拷贝到设备CHECK(cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice));// 创建线程块和线程块网格const int blockSize = TILE_DIM; // 每个线程块的线程数量dim3 threads(blockSize); // 一维线程块dim3 gridSize2(gridSize); // 一维网格// 实现计时cudaEvent_t start, stop;CHECK(cudaEventCreate(&start));CHECK(cudaEventCreate(&stop));CHECK(cudaEventRecord(start));// 调用核函数,进行数组归约Array_sum << <gridSize2, threads >> > (d_A, d_re);CHECK(cudaEventRecord(stop));CHECK(cudaEventSynchronize(stop));float milliseconds = 0;CHECK(cudaEventElapsedTime(&milliseconds, start, stop));// 输出运行时间,单位是msstd::cout << "运行时间:" << milliseconds << "ms" << std::endl;// 将结果 re 从设备拷贝到主机CHECK(cudaMemcpy(h_re, d_re, gridSize * sizeof(float), cudaMemcpyDeviceToHost));// 计算最终结果double total_sum = 0.0;for (int i = 0; i < gridSize; i++) {total_sum += h_re[i];}// 输出结果,精度为小数点后10位std::cout << std::fixed << std::setprecision(6); // 你可以根据需要调整精度std::cout << "最终结果:" << total_sum << std::endl;// 释放主机和设备内存free(h_A);free(h_re);CHECK(cudaFree(d_A));CHECK(cudaFree(d_re));return 0;
}

输出结果
在这里插入图片描述
观察结果又比使用全局内存的CUDA程序快了20ms

1.2.2 分析代码

主要看核函数代码:

  1. 核函数中定义了一个共享内存数组s_y[TILE_DIM];
  2. 通过一个三元运算s_y[tid] = (n < 100000000) ? d_x[n] : 0.0;,来把全局内存的数据复制到共享内存中,数组大小之外的赋值为0,即不对计算产生影响
  3. 调用函数 __syncthreads 进行线程块内的同步
  4. 归约计算过程中,用共享内存变量替换了原来的全局内存变量
  5. 因为共享内存变量的生命周期仅仅在核函数内,所以必须在核函数结束之前将共享内 存中的某些结果保存到全局内存,所以if (tid == 0)判断语句会把共享内存中的数据复制给全局内存

以上的共享内存使用方式叫做使用静态共享内存,因为共享内存数组的长度是固定的。

1.3 使用动态共享内存

在上面的核函数中,我们在定义共享内存数组是一个固定的长度,且程序让该长度和block_size是一致的,但是如果在定义共享内存变量时不小心把数组长度写错了,就有可能引起错误或者降低核函数性能。

有一种方法可以减少这种错误发生的概率,那就是使用动态的共享内存,使用方法如下:

①在核函数调用代码中,写下第三个参数:

// 调用核函数,进行数组归约
Array_sum << <gridSize2, threads,sizeof(float)*blockSize>> > (d_A, d_re);

第三个参数就是核函数中每个线程块需要 定义的动态共享内存的字节数,没写的时候默认是0

②改变核函数中共享内存的声明方式:
使用extern限定词,且不能指定数组大小

extern __shared__ float s_y[];

输出结果变化不大:
在这里插入图片描述

2 例子:矩阵转置

2.1 运行代码

在矩阵转置问题中,对全局内存的读和写这两个 操作,总有一个是合并的,另一个是非合并的,那么利用共享内存可以改善全局内存的访问模式,使得对全局内存的读和写都是合并的,依然使用行索引转列索引,代码如下:

#include <cuda_runtime.h>
#include <iostream>
#include "error_check.cuh"#define TILE_DIM 32  // 定义每个block的线程块维度__global__ void cpy_matrix(const float* A, float* B, const int N) {__shared__ float S[TILE_DIM][TILE_DIM ];  // 动态共享内存不能直接定义二维数组int nx1 = blockIdx.x * TILE_DIM + threadIdx.x;  // 计算当前线程的列索引int ny1 = blockIdx.y * TILE_DIM + threadIdx.y;  // 计算当前线程的行索引if (nx1 < N && ny1 < N) {// 列索引转行索引,实现矩阵转置S[threadIdx.y][threadIdx.x] = A[ny1 * N + nx1];}__syncthreads();// 转置后的线程索引(交换 x 和 y)int nx2 = blockIdx.y * TILE_DIM + threadIdx.x;int ny2 = blockIdx.x * TILE_DIM + threadIdx.y;if (nx2 < N && ny2 < N) {B[ny2 * N + nx2] = S[threadIdx.x][threadIdx.y];  // 从共享内存写入全局内存}
}int main() {// 定义矩阵大小const int N = 1024;const int size = N * N * sizeof(float);// 主机上分配内存float* h_A = (float*)malloc(size);float* h_B = (float*)malloc(size);// 初始化矩阵数据for (int i = 0; i < N * N; i++) {h_A[i] = 1.0f;}// 在设备上分配内存float* d_A, * d_B;CHECK(cudaMalloc((void**)&d_A, size));CHECK(cudaMalloc((void**)&d_B, size));// 将主机矩阵数据拷贝到设备CHECK(cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice));CHECK(cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice));// 创建线程块和线程块网格dim3 threads(TILE_DIM, TILE_DIM);  // 使用32x8的线程块来减少银行冲突dim3 gridSize((N + TILE_DIM - 1) / TILE_DIM, (N + TILE_DIM - 1) / TILE_DIM);// 计算核函数运行时间cudaEvent_t start, stop;CHECK(cudaEventCreate(&start));CHECK(cudaEventCreate(&stop));CHECK(cudaEventRecord(start));// 调用核函数,将矩阵 A 转置并复制到矩阵 Bcpy_matrix << <gridSize, threads >> > (d_A, d_B, N);CHECK(cudaEventRecord(stop));CHECK(cudaEventSynchronize(stop));float milliseconds = 0;CHECK(cudaEventElapsedTime(&milliseconds, start, stop));std::cout << "kernel time: " << milliseconds << "ms" << std::endl;// 将矩阵 B 从设备拷贝到主机CHECK(cudaMemcpy(h_B, d_B, size, cudaMemcpyDeviceToHost));// 释放主机和设备内存free(h_A);free(h_B);cudaFree(d_A);cudaFree(d_B);return 0;
}

输出结果:
在这里插入图片描述
比《CUDA编程》7.全局内存的合理使用中的行索引转列索引的0.35ms要快,但是还是比列索引慢。

2.2 分析代码

if (nx1 < N && ny1 < N) {// 列索引转行索引,实现矩阵转置S[threadIdx.y][threadIdx.x] = A[ny1 * N + nx1];}__syncthreads();// 转置后的线程索引(交换 x 和 y)int nx2 = blockIdx.y * TILE_DIM + threadIdx.x;int ny2 = blockIdx.x * TILE_DIM + threadIdx.y;if (nx2 < N && ny2 < N) {B[ny2 * N + nx2] = S[threadIdx.x][threadIdx.y];  // 从共享内存写入全局内存}
  1. 首先按行索引把全局内存的数据复制到共享内存,这一操作是顺序操作,所以是合并访问(第7章已经讨论过),速度较快
  2. 共享内存数据按照列索引将数据复制回全局内存中去,这一步不是顺序访问,但是由于共享内存速度快,弥补了非合并访速度慢的缺点,所以最后的运行速度也快上不少

3 避免共享内存的bank冲突

关于共享内存,有一个内存 bank 的概念值得注意,。为了获得高的内存带宽,共享内 存在物理上被分为 32 个同样宽度的、能被同时访问的内存 bank。

①bank冲突定义: 多个线程同时访问共享内存的同一个bank,导致这些访问不能被并行处理,从而降低性能,如下示意图:
在这里插入图片描述
②为什么上述代码会产生bank冲突

__shared__ float S[TILE_DIM][TILE_DIM]; // TILE_DIM = 32创建了一个
32x32 的共享内存数组,表示一个总共 1024 个浮点数的数组。

S[threadIdx.y][threadIdx.x] = A[ny1 * N + nx1];这段代码中,每个线程根据其 threadIdx.x 和 threadIdx.y 的值访问共享内存。如果 threadIdx.y 为 0,threadIdx.x 从 0 到 31 的所有线程将依次访问:S[0][0]、S[0][1]、S[0][31]

和上图一比,就发现实际是访问的同步一个bank,所以产生了bank冲突,而且每个bank收到了32个访问,所以是32路bank冲突

③解决方法
通常可以用改变共享内存数组大小的方式来消除或减轻共享内存的 bank 冲突

__shared__ float S[TILE_DIM][TILE_DIM + 1];  // +1 用于避免银行冲突

输出结果:
在这里插入图片描述
解决了bank冲突后,运行速度又提升了,现在只比列索引慢了1ms,所以合理的使用共享内存,可以有效改善非合并访问的性能瓶颈

因为让原本的32列共享内存数组变成了33列,线程的访问模式将会变得更加分散,bank 的访问更加均匀,从而避免了多个线程同时请求同一 bank 的情况。

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

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

相关文章

Git上传命令汇总

进入企业&#xff0c;每日需要上传执行用例记录到gitlab平台上&#xff0c;本文记录了常用git上传命令&#xff0c; 并用github演示。 1、本地建立分支&#xff0c;克隆远程仓库 在gitlab中&#xff0c;每个人需要创建自己的分支&#xff0c;一般以自己的名字命名&#xff0c;…

三子棋(C 语言)

目录 一、游戏设计的整体思路二、各个步骤的代码实现1. 菜单及循环选择的实现2. 棋盘的初始化和显示3. 轮流下棋及结果判断实现4. 结果判断实现 三、所有代码四、总结 一、游戏设计的整体思路 &#xff08;1&#xff09;提供一个菜单让玩家选择人机对战、玩家对战或者退出游戏…

第二十七篇:传输层讲解,TCP系列一

一、传输层的功能 ① 分割与重组数据 传输层也要做数据分割&#xff0c;所以必然也需要做数据重组。 ② 按端口号寻址 IP只能定位数据哪台主机&#xff0c;无法判断数据报文应该交给哪个应用&#xff0c;传输层给每个应用都设置了一个编号&#xff0c;这个编号就是端口&…

Midjourney官宣网页版免费用!前谷歌大佬祭出AI生图2.0,全网惊艳实测

Midjourney一度稳居AI生图的第一梯队&#xff0c;甚至是很多人心中的Top1。但是Ideogram 2.0的发布&#xff0c;抢夺了Midjourney的荣光&#xff0c;不仅一举拉高了图像生成质量&#xff0c;还打起了价格战。 曾经在AI图像生成领域无可匹敌的领导者Midjourney&#xff0c;终于…

3分钟学会下载 blender

1. blender简介 Blender是一款开源的3D创作套件&#xff0c;它由Blender Foundation维护&#xff0c;并得到了全球志愿者和专业开发者的支持。Blender广泛应用于3D模型的制作、动画、渲染、视频编辑、游戏创建、模拟、 composting以及3D打印等多个领域。 功能特点&#xff1a…

欧盟通过《网络弹性法案》保障联网产品安全

欧盟理事会通过了《网络弹性法案》&#xff08;CRA&#xff09;&#xff0c;这是一项新法律&#xff0c;旨在使含有数字组件的消费产品更加安全使用。 CRA要求 CRA 概述了欧盟范围内针对数字产品的网络安全标准&#xff0c;即直接或间接连接到其他设备或网络的产品。 此类别…

Qt-QDockWidget浮动窗口相关操作(49)

目录 描述 使用 描述 在 Qt 中&#xff0c;浮动窗⼝也称之为铆接部件。浮动窗⼝是通过 QDockWidget类 来实现浮动的功能。浮动窗口⼀般是位于核心部件的周围&#xff0c;可以有多个。 使用 创建我们可以参考下面的语法格式 使用起来也很简单&#xff0c;不过只能创建一个 Q…

DAY47WEB 攻防-PHP 应用文件上传函数缺陷条件竞争二次渲染黑白名单JS 绕过

1、PHP-原生态-文件上传-检测后缀&黑白名单2、PHP-原生态-文件上传-检测信息&类型内容3、PHP-原生态-文件上传-函数缺陷&逻辑缺陷4、PHP-原生态-文件上传-版本缺陷&配置缺陷 文件上传安全指的是攻击者通过利用上传实现后门的写入连接后门进行权限控制的安全问题…

05 django管理系统 - 部门管理 - 修改部门

04我们已经实现了新增部门的功能&#xff0c;下面开始修改部门模块的实现。 按道理来说&#xff0c;应该是做成弹框样式的&#xff0c;通过ajax悄咪咪的发数据&#xff0c;然后更新前端数据&#xff0c;但是考虑到实际情况&#xff0c;先用页面跳转的方式实现&#xff0c;后面…

TCP/IP相关

1、关于三次握手、四次挥手和TCP的11种状态&#xff1a; 记住这张图就行了&#xff1a; 2、关于慢启动、拥塞避免、超时重传、快速重传、快速恢复 记住这张图就行了&#xff1a; 一些名词解释&#xff1a; MSS&#xff1a;Maximum Segment Size&#xff0c;最大报文长度 RT…

鸿蒙开发之ArkUI 界面篇 三十四 容器组件Tabs 自定义TabBar

如果需要修改Tabs的图标和文字之间的距离我们该怎么办呢&#xff1f;好在tabBar是联合类型&#xff0c;提供了自定义tabBar&#xff0c;这里就可以显示特殊图标或者是文字图片&#xff0c;如下图&#xff1a; 这里定义了myBuilder的函数&#xff0c;用了 来修饰&#xff0c;没有…

《深度学习》【项目】自然语言处理——情感分析 <上>

目录 一、项目介绍 1、项目任务 2、评论信息内容 3、待思考问题 1&#xff09;目标 2&#xff09;输入字词格式 3&#xff09;每一次传入的词/字的个数是否就是评论的长度 4&#xff09;一条评论如果超过32个词/字怎么处理&#xff1f; 5&#xff09;一条评论如果…

【论文阅读】SRCNN

学习资料 论文题目&#xff1a;Learning a Deep Convolutional Network for Image Super-Resolution&#xff08;学习深度卷积网络用于图像超分辨率&#xff09;论文地址&#xff1a;link.springer.com/content/pdf/10.1007/978-3-319-10593-2_13.pdf代码&#xff1a;作者提出的…

zabbix报警设置

文章目录 I 通过脚本媒介,调用钉钉电话API。配置脚本设置脚本媒介II 配置发送邮件的信息案例:配置163邮箱配置接收媒介创建动作知识扩展DING 2.0 发钉钉钉机器人自定义webhookzabbix执行远程命令I 通过脚本媒介,调用钉钉电话API。 zabbix可以通过脚本调用钉钉电话API,需要…

工程文件参考——STM32+HAL+SPI主从机通讯

文章目录 前言CubeMX设置SPI设置NSS设置 SPI从机代码SPI主机代码 前言 关于如何简单的写一个稳定的SPI主从机通讯&#xff0c;思路很简单 1、SPI高速传输的时候很容易出现错位之类的问题&#xff0c;CRC的校验首先是必要的。在STM32中SPI使用DMA通讯可以自动执行CRC的校验&…

Linux——Harbor(容器镜像 管理项目)

镜像拉取存在一定的问题&#xff0c;出现原因在于&#xff1a; 使用官方公共仓库中的镜像。 拉取的镜像&#xff0c;主要保存在一下仓库中&#xff1a; docker.io //Docker hub 最大的官方维护的公共镜像仓库&#xff0c;一般都会提供所有项目的最新版镜像&#xff0c;镜像…

springboot 整合 快手 移动应用 授权 发布视频 小黄车

前言&#xff1a; 因快手文档混乱&#xff0c;官方社区技术交流仍有很多未解之谜&#xff0c;下面3种文档的定义先区分。 代码中的JSON相关工具均用hutool工具包 1.快手 移动双端 原生SDK 文档https://mp.kuaishou.com/platformDocs/develop/mobile-app/ios.html 2.快手 Api 开…

物联网智能项目(含案例说明)

物联网&#xff08;Internet of Things&#xff0c;简称IoT&#xff09;智能项目是指利用物联网技术将各种物理设备、传感器、软件、网络等连接起来&#xff0c;实现设备之间的互联互通&#xff0c;并通过数据采集、传输、处理和分析&#xff0c;实现智能化管理和控制的项目。以…

Qt入门教程:创建我的第一个小程序

本章教程&#xff0c;主要介绍如何编写一个简单的QT小程序。主要是介绍创建项目的过程。 一、打开QT软件编辑器 这里使用的是QT5.14.2版本的&#xff0c;安装教程参考以往教程&#xff1a;https://blog.csdn.net/qq_19309473/article/details/142907096 二、创建项目 到这里&am…

【Docker】03-自制镜像

1. 自制镜像 2. Dockerfile # 基础镜像 FROM openjdk:11.0-jre-buster # 设定时区 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # 拷贝jar包 COPY docker-demo.jar /app.jar # 入口 ENTRYPOINT ["ja…