news 2026/10/23 6:42:27

线性回归Java实现:梯度下降与正态方程从原理到避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线性回归Java实现:梯度下降与正态方程从原理到避坑

简介:面向Java初学者与机器学习入门者的线性回归实现,涵盖梯度下降与正态方程两种求解方式。项目基于Andrew Ng公开课中的经典示例数据,通过拟合y=Mx+B展示从训练集学习参数的过程,同时支持高阶多项式特征扩展,帮助理解监督学习核心思想。压缩包共13个文件,以5个txt训练数据集、3个java源码、1个JAMA矩阵库jar包为主,另含README说明与工程配置文件,整体仅44KB,结构精简适合快速阅读。已有252人学习下载,适合想用Java动手实现基础算法的开发者。通过源码可对照两种方法的迭代更新与矩阵推导,配合多组示例数据直观观察拟合效果,是巩固线性回归原理的实用参考。

1. 线性回归不是调包:手写梯度下降与正态方程的Java实现值得过一遍

有些人觉得线性回归是入门级玩具,等真到Java工程里自己实现一遍带梯度下降和正态方程的完整版本,才发现参数怎么更新、收敛条件怎么判、矩阵什么时候不可逆,每一处都直接影响预测效果。这份Linear-Regression项目把两条经典路径完整落地:梯度下降走迭代逼近,适合特征多、数据量大的场景;正态方程走矩阵解析解,适合特征维度不高的小数据集。我拆这套代码的初衷,是想摆脱机器学习里默认的调包习惯,用纯Java数组把每一步循环、每一个求导都摊开看。想搞懂线性回归内部机制的Java开发者,或者需要对比两种算法边界的人,适合拿源码直接过一遍。

2. 两种求解路径的选型逻辑:梯度下降和正态方程各管各的战场

这一章先把代价函数立起来,再讲梯度下降三种变体的取舍,最后说明正态方程的适用边界。选型逻辑搞清楚之后,后面看代码就不会只是照抄能跑,而是知道什么时候该用哪条路。

2.1 代价函数J(θ):梯度下降和正态方程都在优化同一个目标

线性回归要做的事情,就是找一组参数θ,让预测值和真实值之间的误差最小。这个误差通常用均方误差衡量,写成数学形式就是J(θ) = (1 / (2m)) Σ(h_θ(x^(i)) - y^(i))^2。这里的h_θ(x^(i))是第i条样本的预测值,y^(i)是真实标签,m是样本数量。分母里的2不是必须的,纯粹是为了后面求导时把平方项的2消掉,让梯度表达式更干净。这一点很多入门教程不提,但自己推导一遍就会明白为什么代价函数普遍长这样。

为什么不直接用绝对误差?因为平方函数在整个定义域内光滑可导,梯度下降每一步都能拿到稳定的方向;绝对值函数在零点有尖角,参数一过零点梯度方向就会跳动,实际跑起来代价曲线容易抖动。如果你把两种损失函数都实现一遍对比,会发现平方误差的收敛路径明显更顺滑。

在这一版Java实现里,代价计算就是两层循环:外层遍历样本,内层遍历特征做点积得到预测值,再把误差平方累加。不需要引入任何矩阵库,原生double数组就能搞定。只要把computeCost方法里的循环写对,结果的数值精度和调包版本是一致的。

2.2 批量梯度下降、随机梯度下降、小批量:三选一怎么取舍

梯度下降的核心更新规则是θ_j := θ_j - α * (1/m) * Σ(h_θ(x^(i)) - y^(i)) * x_j^(i)。这个公式拆开就是三件事:算预测值、算误差、把误差乘上对应特征值后求和。α是学习率,决定每次往负梯度方向走多远;x_j^(i)是第i条样本的第j个特征。

资源包默认跑批量梯度下降,也就是每次更新都遍历全量样本,梯度方向最稳定。当特征数几万、样本几十万时,批量版本每轮迭代都要全量扫描,速度会很难看。随机梯度下降每次随机抽一条样本就更新一次,迭代快但收敛路径很花,最后会在最优点附近小幅跳动。小批量是中间态,把数据切成窗口,每个窗口算一次梯度。Java实现里常见做法是给梯度下降方法加一个batchSize参数,默认传m就是批量,传32或64就是小批量。

选型的工程经验是:样本量小于一万,批量梯度下降完全够用;样本量更大,优先小批量,batchSize从32开始试,迭代轮数要跟着加大;随机梯度下降不推荐直接上生产,抖动太大,调试也不方便。还有一个容易被忽略的点:梯度下降实现里同步更新和异步更新的结果差异很大,后面第3章代码会直接展示同步更新的写法。

2.3 正态方程何时有机会:特征维度不高时的干净解

正态方程的思路跟梯度下降完全不同,它不对代价函数做迭代,而是直接求导并令导数为零,解出解析解θ = (X^T X)^(-1) X^T y。这套解法的前提是X^T X可逆。它没有学习率,没有迭代轮数,不需要特征缩放,几行矩阵运算就能得到和梯度下降收敛后几乎一样的参数。代价是理论复杂度O(n^3),这里n是特征维度。

我一般以特征数10000作为粗略分界线:低于这个值,正态方程秒出结果,代码短、无调参烦恼;高于这个值,矩阵运算时间和内存占用都会陡增,这时候老老实实切回梯度下降。还要注意一个隐藏问题:X^T X的列与列之间如果存在线性相关,比如同时放了一列摄氏温度和一列华氏温度,求逆就会失败。这是后面避坑章节重点排查的对象,也是为什么正则化不仅用于防过拟合,还能顺手解决矩阵奇异的问题。

3. 把梯度下降写成Java代码:特征缩放、主循环与收敛监控

3.1 数据装载与特征缩放:先处理好二维数组再谈算法

Java处理CSV没什么魔法,逐行读、按逗号切分、转成double就能塞进特征矩阵。资源包里数据的组织方式是:X是m×n的二维数组,y是长度为m的一维数组,theta长度是n+1,下标0留给偏置项。跟常见的Python实现相比,它只是把numpy换成了显式循环,正好方便你观察每一步。

梯度下降对特征的量纲极其敏感。假设一个特征取值范围是0到100000,另一个是0到5,代价函数的等高线会被拉成很扁的椭圆,梯度方向忽左忽右,学习率稍不合适就会发散或半天走不到位。所以正规实现里都会先做归一化。

/** * 零均值、单位方差归一化,按列原地处理 * X: m x n 特征矩阵,m为样本数,n为特征数 */ public static void normalize(double[][] X, int m, int n) { for (int j = 0; j < n; j++) { double sum = 0.0; for (int i = 0; i < m; i++) { sum += X[i][j]; } double mean = sum / m; double variance = 0.0; for (int i = 0; i < m; i++) { variance += Math.pow(X[i][j] - mean, 2); } double std = Math.sqrt(variance / m); // 常数列的标准差为0,跳过避免除零 if (std == 0.0) { continue; } for (int i = 0; i < m; i++) { X[i][j] = (X[i][j] - mean) / std; } } }

这里按列先算均值,再算总体标准差,最后把列内每个值减去均值除以标准差,让该列变成均值接近0、标准差量级接近1。std等于0说明这一列所有样本值相同,对预测没有帮助,直接跳过而非除零。

关于归一化有一个必须记住的点:训练数据上算出来的mean和std要保存下来,预测新样本时用同一套参数做变换。如果在预测阶段直接套normalize方法重新算统计量,模型会失效。这是最容易被忽视的坑,后面避坑章节还会回到这个话题。

3.2 梯度下降主循环:同步更新、代价记录与停止条件

主循环最好写成独立的gradientDescent方法,输入特征矩阵、标签、初始theta、学习率alpha、迭代轮数iterations,返回代价历史数组。这样每次调整参数后,可以把代价历史拉出来看曲线,判断是震荡还是平稳下降。

/** * 批量梯度下降 * @param alpha 学习率,常见先取 0.01 * @param iterations 最大迭代轮数,资源包默认 1000 */ public static double[] gradientDescent(double[][] X, double[] y, double[] theta, double alpha, int iterations) { int m = X.length; int n = theta.length; double[] costHistory = new double[iterations]; for (int iter = 0; iter < iterations; iter++) { // 计算所有梯度,全部存完再统一更新,避免串行污染 double[] grad = new double[n]; for (int j = 0; j < n; j++) { double sum = 0.0; for (int i = 0; i < m; i++) { double prediction = predict(X[i], theta); sum += (prediction - y[i]) * X[i][j]; } grad[j] = sum / m; } // 同步更新:所有梯度和旧theta计算完成后再更新 for (int j = 0; j < n; j++) { theta[j] -= alpha * grad[j]; } costHistory[iter] = computeCost(X, y, theta); } return costHistory; } public static double predict(double[] row, double[] theta) { double sum = 0.0; for (int j = 0; j < theta.length; j++) { sum += row[j] * theta[j]; } return sum; } public static double computeCost(double[][] X, double[] y, double[] theta) { int m = X.length; double cost = 0.0; for (int i = 0; i < m; i++) { double error = predict(X[i], theta) - y[i]; cost += error * error; } return cost / (2 * m); }

这段代码的逻辑是:先为n个参数分别累加误差乘以对应特征值的总和,再除以样本数得到梯度分量;所有梯度的计算都基于旧theta,grad数组填满之后才统一更新theta。如果换成内循环边算边更新,后面的梯度会混合新参数,代价曲线容易上下跳动,正是不少网上代码"抄了但不收敛"案例的根源。

predict方法做的是特征向量与theta的点积,computeCost返回当前参数下的均方误差。alpha参数建议从0.01开始,训练后如果发现costHistory整体趋势下降得太慢,再往下调一个量级;如果出现cost递增,说明alpha太大,也要往下调。迭代轮数在特征多的情况下要加大到5000甚至10000,默认1000只是给低维度数据用的起点值。

3.3 收敛监控与预测阶段:别只凭固定轮数收工

如果只跑固定迭代次数,很容易出现两种假象:alpha太小,1000轮后cost还在缓慢下降,模型远远没到最优;alpha合适,但300轮就到了平坦区,后面都在原地磨。更稳的做法是把收敛判断独立出来,让循环在代价变化量低于阈值时提前退出。

// 用 while 循环替代固定 for,双重条件判断收敛 int iter = 0; double epsilon = 1e-6; double[] costHistory = new double[maxIterations]; double lastCost = Double.MAX_VALUE; while (iter < maxIterations) { // 这里复用 gradientDescent 里的单轮更新逻辑 gradientStep(X, y, theta, alpha); costHistory[iter] = computeCost(X, y, theta); if (Math.abs(costHistory[iter] - lastCost) < epsilon) { break; } lastCost = costHistory[iter]; iter++; }

epsilon取1e-6是常见做法,含义是连续两轮代价变化小于百万分之一就认为收敛。这里把收敛条件、最大轮数、代价记录三者分开,排查问题时可以分别调整。实际调试时,我一般每100轮打印一次cost,而不是等全部跑完再看结果,跑完发现发散还得重新调参,等于浪费一轮等待。

训练结束后,theta数组就是要保存的模型参数。Java里最朴素的做法是用ObjectOutputStream把double[]写进文件,预测时再读出来。预测阶段要做同样的归一化,这一条只要忘一次,模型的预测值就会和训练时的量纲完全对不上。

4. 正态方程的工程实现:矩阵运算与两条路径的边界条件

4.1 从高斯消元到参数求解:不依赖第三方库的矩阵求逆

正态方程在Java里最麻烦的部分就是求逆。项目包里没有引入外部矩阵库,而是用列主元高斯消元手工实现了。列主元的意思是,每次消元前先在当前列中找到绝对值最大的行,把那一行换到主元位置,避免对角线上出现接近0的小数导致除法误差被放大。

/** * 列主元高斯-约当消元求逆(增广矩阵法) * @param matrix 需要求逆的 n x n 方阵 * @return 逆矩阵 */ public static double[][] inverse(double[][] matrix, int n) { // 构造 n x 2n 增广矩阵,右侧放单位矩阵 double[][] a = new double[n][2 * n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { a[i][j] = matrix[i][j]; } a[i][n + i] = 1.0; } for (int col = 0; col < n; col++) { // 选择主元:当前列绝对值最大的行 int pivot = col; double maxAbs = Math.abs(a[col][col]); for (int row = col + 1; row < n; row++) { if (Math.abs(a[row][col]) > maxAbs) { maxAbs = Math.abs(a[row][col]); pivot = row; } } if (maxAbs < 1e-12) { throw new IllegalArgumentException("矩阵不可逆,检查特征列是否存在线性相关"); } // 交换主元行 double[] tmp = a[col]; a[col] = a[pivot]; a[pivot] = tmp; // 主元行归一化 double div = a[col][col]; for (int j = 0; j < 2 * n; j++) { a[col][j] /= div; } // 用主元行消去其他行的当前列 for (int row = 0; row < n; row++) { if (row == col) { continue; } double factor = a[row][col]; if (Math.abs(factor) < 1e-15) { continue; } for (int j = 0; j < 2 * n; j++) { a[row][j] -= factor * a[col][j]; } } } // 右侧部分就是逆矩阵 double[][] result = new double[n][n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { result[i][j] = a[i][j + n]; } } return result; }

这段代码的关键点有三个:增广矩阵的构造方式,右侧放单位矩阵后整体行变换,左侧变成单位矩阵时右侧就是逆矩阵;列主元选择避免小主元带来的误差;最后一步把增广矩阵的右半部分取出来。maxAbs小于1e-12的判定,就是前面提到矩阵奇异时的显式报错位置。

4.2 正态方程的组装与边界条件:什么时候比梯度下降更划算

有了inverse方法,正态方程的实现就只剩矩阵乘法。核心代码逻辑是:先给特征矩阵最右侧拼一列1.0作为偏置项对应的输入,然后算X^T X、对它求逆、再乘X^T y。这个过程一步到位,没有任何迭代参数。

public static double[] normalEquation(double[][] X, double[] y, int m, int n) { // X的维度扩展为 m x (n+1),最后一列全1 double[][] xExtended = new double[m][n + 1]; for (int i = 0; i < m; i++) { System.arraycopy(X[i], 0, xExtended[i], 0, n); xExtended[i][n] = 1.0; } double[][] xt = transpose(xExtended, m, n + 1); // (n+1) x m double[][] xtx = multiply(xt, xExtended, n + 1, m, n + 1); // (n+1) x (n+1) double[][] inv = inverse(xtx, n + 1); double[][] xty = multiply(xt, toColumnMatrix(y), n + 1, m, 1); // (n+1) x 1 double[][] thetaMatrix = multiply(inv, xty, n + 1, n + 1, 1); return toArray(thetaMatrix, n + 1); }

代码里transpose负责转置、multiply负责矩阵乘法、toColumnMatrix把一维y包装成m×1矩阵,这三个工具方法各自几十行,思路都不复杂,资源包里已经给出完整实现。组装逻辑上,偏置项是显式拼出来的,这跟梯度下降里theta[0]默认对应全1列的做法保持一致。

工程取舍很直接,可以用一张表说清楚:

对比维度批量梯度下降正态方程
特征数n无硬上限,内存和耗时随n线性增长10000以下推荐,超过则O(n^3)吃力
样本量m每轮迭代O(m),大样本要切小批量主要看矩阵维度,m大时仍可接受
是否迭代是,需要调学习率和迭代轮数否,一步求解
特征缩放必须不需要
主要风险学习率、缩放、收敛判断矩阵不可逆、内存占用

样本量m对两种方法的影响也不同:梯度下降每轮代价都和m成正比,m过大要切小批量;正态方程只在乎矩阵维度,m大一点问题不大,只要内存放得下。

4.3 加正则化:一条路保数值稳定,另一条路防过拟合

当X^T X不可逆时,一个后悔药是加岭回归正则项,把求逆对象从X^T X改成X^T X + λI,其中I是单位矩阵,λ是正则系数。加了之后矩阵大概率变成严格正定,原本线性相关的特征列也不会让求逆直接崩溃。

梯度下降版的正则化稍微绕一点,代价函数多了惩罚项,梯度更新也要跟着改。标准写法是偏置项θ_0不参与正则,其他参数每轮更新时额外衰减一点。工程上为省事也有统一加正则的版本,特征维度不高时两者差别不大。

// 梯度下降单轮更新里带 L2 正则,lambda 控制惩罚强度 double lambda = 1.0; for (int j = 0; j < n; j++) { if (j == 0) { // 偏置项不做正则,保持原更新方式 theta[j] -= alpha * grad[j]; } else { theta[j] -= alpha * (grad[j] + (lambda / m) * theta[j]); } }

这个写法把正则项lambda/m乘以theta[j]直接并入梯度,效果是每次更新让非偏置参数向零压缩一点。lambda太小等于没加,太大则所有特征都被压向零,模型变成接近常数的预测。一般取值从0.1、1.0、10.0这档试。正态方程那边更简单,把inverse方法的输入换成X^T X + λI即可,λ从1.0开始调。

5. 避坑:五个让梯度下降翻车的常见问题与排查

这一章直接把我复现过程中踩过的坑按现象、原因、解决的顺序列出来。绝大多数问题都不在算法公式里,而藏在数据预处理和参数设置这种不起眼的位置。

5.1 训练到一半cost输出NaN,参数变成无穷大

现象:前几轮代价函数还在下降,接着某一步突然出现NaN,后续theta数组也全部变成Infinity或NaN。

原因:最常见的是特征量纲差异太大且没有做归一化。比如某个特征是总房价,量级在百万,另一个是房间数,量级在个位。学习率对所有参数用同一个值,量级大的维度梯度爆炸,一步更新就把参数推出数值有效范围。

解决:先把normalize方法跑一遍,再看是否还有NaN;同时把学习率从0.01降到0.001重新验证。注意预测阶段也要用训练时保存的mean和std转换输入,否则模型直接废掉。我给数据加载统一预检一次标准差,凡是std为0的列直接丢弃或标记,从源头避免异常。

5.2 学习率过大导致代价递增:别硬着头皮跑完

现象:costHistory数组里的代价随迭代次数递增,甚至出现周期性跳变,theta值也在正负之间来回摆动。

原因:学习率太大,每次更新跨过了代价函数的谷底,走到对面更高的位置。凸函数的代价曲线上还能往回弹,非凸损失里可能跳进更差的区域。

解决:最直接的做法是每轮外层先记录当前theta,cost如果比上一轮上升就回退到旧参数并把alpha减半,重新迭代。我一般会用一组按数量级排的alpha做小实验:0.001、0.003、0.01、0.03、0.1,每个跑几十轮看cost趋势,选出下降最快且不发散的那个,再拿它跑完整训练。这比凭感觉填一个值盲猜靠谱得多。

5.3 梯度下降的维度一直对不上,报数组越界

现象:训练主循环里抛ArrayIndexOutOfBoundsException,或者theta初始化长度是n但特征矩阵X前面又拼了一列全1,两边的维度逻辑不一致。

原因:偏置项的实现方式不统一。有的代码把1.0列拼在特征矩阵里,theta长度取n+1;有的代码不拼列,靠theta[0]固定对应常数项。两种做法混着用时,索引一错就整个崩。

解决:在数据装载阶段就确定一种约定。我习惯采取"拼列"风格:X每行末尾追加一个1.0,theta长度是n+1,predict里不用特判,直接点积。这样三个模块(归一化、主循环、预测)的维度含义始终一致。如果你的训练代码改了拼列方式,预测代码也必须同步改,否则上线时突然多出一维或少一维。

5.4 正态方程报"矩阵不可逆",程序直接中断

现象:调用inverse时抛出"矩阵不可逆"异常,或者高斯消元主元选出来还是接近0的数值,算出的theta奇大无比。

原因:特征列线性相关,比如特征里同时放了摄氏温度和华氏温度;或者样本数量小于特征维度,m < n时X^T X必然不满秩。前者是数据问题,后者是建模问题。

解决:先检查特征列之间的相关系数,高度相关的列删掉一列。若样本量确实少于特征数,要么增加样本,要么转用加了L2正则的正态方程形式。加上λI之后的矩阵大概率严格正定,求逆不会再报错。我这里把正则当成保护机制而不仅是防过拟合工具,每次建模都带着正则项去跑。

5.5 迭代很多轮但代价下降极慢,总觉得再跑跑就能收敛

现象:costHistory每轮都在下降,但降幅微小,10000轮后和2000轮时的代价差距不到0.01,theta还在缓慢漂移。

原因:学习率太小,或者特征缩放做得不彻底。注意归一化里如果用总体标准差而不是样本标准差,数值上会有些偏差,虽然不至于发散,但可能让收敛速度变慢。

解决:先把数据缩放标准统一,再尝试把alpha按10倍步长加大。同时把收敛阈值epsilon从1e-6放宽到1e-5验证一下,如果代价曲线已经在缓慢下滑,说明问题是alpha不够大而不是算法有问题。真正好的学习率应该让cost在前100轮就出现明显下降,后面再进入平滑逼近阶段。

6. 让模型效果再进一步:调参与验证的落地技巧

6.1 用代价历史曲线决定学习率,不看玄学看趋势

调学习率最朴素的落地方式,是每轮把costHistory记录写出来,不用画图也能判断:每隔100轮打印一次J(θ)。如果前100轮cost从10掉到0.5,后面还在稳步下降,这个学习率方向就对了;如果打印出来的数值忽高忽低,立即停掉并减小alpha。更细的做法是把costHistory整体写到CSV,用任何作图工具画一条曲线,比只看几个打印值直观得多。

// 每100轮打印一次代价,观察下降幅度再决定要不要停 for (int iter = 0; iter < maxIterations; iter++) { gradientStep(X, y, theta, alpha); if (iter % 100 == 0) { double c = computeCost(X, y, theta); System.out.printf("iter=%d cost=%.6f%n", iter, c); } }

我一般先跑300轮,看打印出来的cost从初值降到什么量级。如果300轮内降了一个数量级以上,alpha还是太小,调到0.03再试;如果前几轮就出现NaN,alpha太大,降到0.003。几次就能锁定一个合适的量级。

6.2 特征工程的优先级:多项式特征加在归一化之前

线性回归对非线性关系无能为力,但可以用多项式特征逼近。把某个特征x的平方、立方或者两个特征的乘积塞进特征矩阵,模型就能拟合曲线。注意顺序:先构造多项式特征,再做归一化,否则平方项和原始项的量纲差异会让梯度爆炸。加了多项式特征之后,特征数会膨胀,如果同时跑正态方程,要提前评估矩阵维度的内存占用。

从那以后我每次做线性回归Demo,都强制自己先把costHistory可视化跑一遍,再开始调参数和特征,这一步省掉的排查时间远比写代码的时间多。这份Java实现值得按上面的步骤完整过一遍,尤其适合想摆脱调包、独立实现一遍梯度下降和正态方程的开发者。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 6:42:22

手动部署DeepSeek:Linux下CUDA环境配置与避坑指南

简介&#xff1a;这份PDF面向需要在Linux环境下部署DeepSeek的开发者与深度学习爱好者&#xff0c;系统梳理了从环境准备到模型运行的手动部署流程&#xff0c;适合具备一定Linux操作基础、希望独立完成本地部署的中级用户参考。资源包共1个PDF文件&#xff0c;大小约215KB&…

作者头像 李华
网站建设 2026/10/23 6:41:31

Carrier-in-Carrier:卫星通信载波重叠与自适应抵消技术解析

简介&#xff1a;CDM-625高级卫星调制解调器汉译版技术手册&#xff0c;面向卫星通信工程师、系统集成商及运维人员&#xff0c;系统讲解Comtech EF Data CDM-625的核心能力。内容涵盖双端对讲载波内载波带宽压缩技术的自适应抵消原理&#xff0c;VERSAFEC、LDPC、TPC等前向纠错…

作者头像 李华
网站建设 2026/10/23 6:40:18

分布式锁的代码实现:从原理到实战

1. 引言在分布式系统中&#xff0c;多个服务实例可能同时操作共享资源&#xff0c;如果缺乏互斥机制&#xff0c;就会出现超卖、重复扣款、数据覆盖等问题。分布式锁正是为了解决这类跨进程、跨节点的并发竞争问题而设计的通用方案。本文围绕分布式锁的核心原理&#xff0c;给出…

作者头像 李华
网站建设 2026/10/23 6:39:40

Thread 类的基本用法

一.线程创建第一方式&#xff1a;创建子类继承Thread 重写run方法package Thread_star;public class Dome1 extends Thread {Overridepublic void run() {while (true){System.out.println("hello thread");try {Thread.sleep(1_000);} catch (InterruptedException …

作者头像 李华
网站建设 2026/10/23 6:38:34

全文 - 第01章 - 简介 - InfiniBand Specification release 1.4

「译注&#xff1a;本章适合浏览&#xff0c;不需要精读」 本文档是 InfiniBand 架构规范的第一卷。它是描述该架构的一系列文档中的第一份。 1.1 致谢 以下人员在 InfiniBand 架构规范本卷的编写过程中做出了重要贡献&#xff1a; 指导委员会成员 联合主席&#xff1a; 成员&a…

作者头像 李华
网站建设 2026/10/23 6:38:15

【译】今天我将审核 GitHub 拉取请求

今天我将向大家演示&#xff0c;在开发团队协作或者管理开源项目时&#xff0c;如何处理最常见的工作之一&#xff1a;拉取请求&#xff08;Pull Request&#xff09;。本次演示我们会使用 Visual Studio 内置的 GitHub 工具&#xff0c;不过这套操作思路在 Azure DevOps 中也大…

作者头像 李华