• [技术干货] 新手向:C语言、Java、Python 的选择与未来指南
    你好,编程世界的新朋友!当你第一次踏入代码的宇宙,面对形形色色的编程语言,是否感到眼花缭乱?今天我们就来聊聊最主流的三种编程语言——C语言、Java 和 Python——它们各自是谁,适合做什么,以及未来十年谁能带你走得更远。一、编程世界的三把钥匙:角色定位如果把编程比作建造房屋,那么:C语言是钢筋骨架:诞生于1972年,它直接与计算机硬件“对话”,负责构建最基础的支撑结构。Java是精装套房:1995年问世,以“一次编写,到处运行”闻名,擅长打造稳定、可复用的功能模块。Python是智能管家:1991年出生却在近十年大放异彩,像一位高效助手,用最少的指令完成复杂任务13。二、核心差异对比:从底层到应用1. 语言类型与设计哲学C语言:属于面向过程的编译型语言。代码在执行前需全部翻译成机器指令,运行效率极高,但需要开发者手动管理内存(类似自己打扫房间)15。Java:面向对象的半编译语言。代码先转为字节码,再通过Java虚拟机(JVM)运行。牺牲少许效率换来跨平台能力——Windows、Linux、Mac 都能执行同一份代码39。Python:多范式的解释型语言。代码边翻译边执行,开发便捷但速度较慢。支持面向对象、函数式编程,语法如英语般直白78。翻译2. 语法与学习曲线# Python 打印10次"Hello" for i in range(10):     print("Hello") // Java 实现相同功能public class Main {    public static void main(String[] args) {        for(int i=0; i<10; i++){            System.out.println("Hello");        }    }} /* C语言版本 */#include <stdio.h>int main() {    for(int i=0; i<10; i++){        printf("Hello\n");    }    return 0;}运行项目并下载源码Python 接近自然语言,新手1天就能写出实用脚本5Java 需理解类、对象等概念,1-2个月可入门9C语言 需掌握指针、内存分配,门槛最高13. 性能特点语言    执行速度    内存管理    典型场景C语言    手动管理    实时系统、高频交易Java    自动回收    企业后台服务Python   自动回收    数据分析、原型开发C语言直接操作硬件,速度可比Python快50倍以上;Java居中;Python虽慢但可通过C扩展提速210。4. 应用领域C语言:操作系统(Linux内核)、嵌入式设备(空调芯片)、游戏引擎(Unity底层)27Java:    - 安卓APP(微信、支付宝)    - 银行交易系统(高可靠性必须)    - 大型网站后端(淘宝、京东)28Python:    - 人工智能(ChatGPT的基石语言)    - 数据分析(处理百万行Excel只需几行代码)    - 自动化脚本(批量处理文件/网页)185. 生态系统支持Python:拥有28万个第三方库,如NumPy(科学计算)、TensorFlow(AI)2Java:Spring框架统治企业开发,Android SDK构建移动应用2C语言:标准库较小,但Linux/Windows API均以其为核心7三、未来十年:谁主沉浮?1. AI战场:Python 正面临 Java 的挑战Python目前占据90%的AI项目,但2025年可能成为转折点。Java凭借企业级性能正加速渗透:    - Spring AI项目获阿里等巨头支持    - 直接调用GPU提升计算效率(Project Babylon)    - 大厂倾向将AI集成到现有Java系统中46Python 仍靠易用性守住数据科学家阵地,但需解决性能瓶颈10。2. 新兴领域卡位战边缘计算(IoT设备):C语言因极致效率成为传感器、工控设备首选10云原生服务:Java和Go语言(非本文主角)主导容器化微服务8Web3与区块链:Java的强安全性被蚂蚁链等采用23. 就业市场真相Java:国内70%企业系统基于Java,岗位需求最稳定68Python:AI工程师平均薪资比Java高18%,但竞争加剧8C语言:嵌入式开发缺口大,入行门槛高但职业生涯长9四、给新手的终极建议学习路径规划:零基础入门:选 Python → 快速建立成就感,两周做出小工具求职导向:学 Java → 进入金融/电信等行业的核心系统硬件/高薪偏好:攻 C语言 → 深耕芯片、自动驾驶等高端领域关键决策原则:graph LRA[你的目标] --> B{选择语言}B -->|做AI/数据分析| C(Python)B -->|开发企业软件/安卓APP| D(Java)B -->|写操作系统/驱动/引擎| E(C语言)运行项目并下载源码专家提醒:2025年之后,掌握“双语言能力”更吃香:Python + C:用Python开发AI原型,C语言加速核心模块Java + Python:Java构建系统,Python集成智能组件五、技术架构深度拆解1. C语言:系统级开发的基石内存操作直接通过malloc()/free()管理内存,程序员可精确控制每一字节:int *arr = (int*)malloc(10 * sizeof(int)); // 申请40字节内存free(arr); // 必须手动释放,否则内存泄漏运行项目并下载源码指针的威力与风险指针直接访问物理地址,可实现高效数据传递:void swap(int *a, int *b) { // 通过指针交换变量    int temp = *a;    *a = *b;    *b = temp;}运行项目并下载源码典型事故:缓冲区溢出(如strcpy未检查长度导致系统崩溃)应用场景扩展领域    代表项目    关键技术点操作系统    Linux内核    进程调度、文件系统实现嵌入式系统    无人机飞控    实时响应(<1ms延迟)高频交易    证券交易所系统    微秒级订单处理图形渲染    OpenGL底层    GPU指令优化2. Java:企业级生态的王者JVM虚拟机机制Java源码 → 字节码 → JIT编译 → 机器码跨平台原理:同一份.class文件可在Windows/Linux/Mac的JVM上运行垃圾回收(GC)奥秘分代收集策略:graph LRA[新对象] --> B[年轻代-Eden区]B -->|Minor GC| C[Survivor区]C -->|年龄阈值| D[老年代]D -->|Full GC| E[回收]运行项目并下载源码调优关键:-Xmx设置堆大小,G1GC减少停顿时间企业级框架矩阵框架    作用    代表应用Spring Boot    快速构建微服务    阿里双11后台Hibernate    对象-数据库映射    银行客户管理系统Apache Kafka    高吞吐量消息队列    美团订单分发系统Netty    高性能网络通信    微信消息推送3. Python:科学计算的终极武器动态类型双刃剑graph TD  A[数据获取] --> B(Pandas处理)  B --> C{建模选择}  C --> D[机器学习-scikit-learn]  C --> E[深度学习-TensorFlow/PyTorch]  D --> F[模型部署-Flask]  E --> F  F --> G[Web服务]运行项目并下载源码六、行业应用全景图1. C语言:硬科技核心载体航天控制火星探测器着陆程序:实时计算轨道参数(C代码执行速度比Python快400倍)火箭燃料控制系统:直接操作传感器寄存器汽车电子特斯拉Autopilot底层:毫米波雷达信号处理发动机ECU(电子控制单元):微控制器(MCU)仅支持C工业自动化PLC编程:三菱FX系列用C编写逻辑控制数控机床:实时位置控制精度达0.001mm2. Java:商业系统支柱金融科技支付清算:Visa每秒处理6.5万笔交易(Java+Oracle)风控系统:实时反欺诈检测(Apache Flink流计算)电信领域5G核心网:爱立信Cloud RAN基于Java微服务计费系统:中国移动月账单生成(处理PB级数据)电子商务淘宝商品搜索:Elasticsearch集群(Java开发)京东库存管理:Spring Cloud微服务架构3. Python:数据智能引擎生物医药基因序列分析:Biopython处理FASTA文件药物分子模拟:RDKit库计算3D结构金融分析量化交易:pandas清洗行情数据,TA-Lib技术指标计算风险建模:Monte Carlo模拟预测股价波动AIGC革命Stable Diffusion:PyTorch实现文生图大模型训练:Hugging Face Transformers库七、性能优化实战对比1. 计算圆周率(1亿次迭代)// C语言版:0.8秒#include <stdio.h>int main() {    double pi = 0;    for (int k = 0; k < 100000000; k++) {        pi += (k % 2 ? -1.0 : 1.0) / (2*k + 1);    }    printf("%f", pi * 4);}运行项目并下载源码// Java版:1.2秒public class Pi {    public static void main(String[] args) {        double pi = 0;        for (int k = 0; k < 100000000; k++) {            pi += (k % 2 == 0 ? 1.0 : -1.0) / (2*k + 1);        }        System.out.println(pi * 4);    }}运行项目并下载源码# Python版:12.7秒 → 用Numpy优化后:1.5秒import numpy as npk = np.arange(100000000)pi = np.sum((-1)**k / (2*k + 1)) * 4print(pi)运行项目并下载源码2. 内存消耗对比(处理1GB数据)语言    峰值内存    关键影响因素C    1.1GB    手动分配精确控制Java    2.3GB    JVM堆内存开销Python    5.8GB    对象模型额外开销八、未来十年技术演进预测1. C语言:拥抱现代安全特性新标准演进:C23引入#elifdef简化宏,nullptr替代NULL安全强化:边界检查函数(如strcpy_s())静态分析工具(Clang Analyzer)2. Java:云原生时代进化GraalVM革命:将Java字节码直接编译为本地机器码(启动速度提升50倍)Project Loom:虚拟线程支持百万级并发(颠覆传统线程模型)3. Python:性能突围计划Pyston v3:JIT编译器使速度提升30%Mojo语言:兼容Python语法的超集,速度达C级别(专为AI设计)九、开发者能力矩阵建议能力维度    C语言工程师    Java架构师    Python数据科学家核心技能    指针/内存管理    Spring Cloud生态    Pandas/NumPy汇编接口调用    JVM调优    Scikit-Learn实时系统设计    分布式事务    TensorFlow辅助工具    GDB调试器    Arthas诊断工具    Jupyter NotebookValgrind内存检测    Prometheus监控    MLflow实验管理薪资范围    3-5年经验:30-50万    5-8年经验:50-80万    AI方向:60-100万+结语:三角平衡的编程生态C语言守护数字世界的物理边界——没有它,芯片无法启动,火箭不能升空Java构筑商业文明的数字基石——支撑全球70%的企业交易系统Python点燃智能时代的创新引擎——驱动90%的AI研究论文————————————————原文链接:https://blog.csdn.net/2302_77626561/article/details/151645868
  • [技术干货] 数据结构-5.Java. 二叉树
    本篇博客给大家带来的是二叉树的知识点, 其中包括面试经常会提问的真题 ArrayList 和 LinkedList 的区别 .如果你不知道分享给谁,那就分享给薯条。你们的支持是我不断创作的动力 .1. 二叉树1.1 二叉树的概念一棵二叉树是结点的一个有限集合,该集合:1. 或者为空2. 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。1.2 两种特殊的二叉树1. 满二叉树: 一颗二叉树, 如果每层的结点数都达到最大值, 则这颗二叉树就是满二叉树. 若满二叉树的层数为K, 那么其节点总数是 2^k - 1. 2. 完全二叉树: 完全二叉树是效率很高的数据结构, 完全二叉树是由满二叉树 引出来的。对于深度为K 有 n个节点的二叉树, 当且仅当其每一个节点都与深度为K 的 满二叉树中编号从0 至 n-1的节点一一对应时 称之为完全二叉树. 满二叉树其实就是一种特殊的完全二叉树.1.3二叉树的性质1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1) (i>0)个结点.2. 若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 2^k  -  1 (k>=0).3. 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+14. 具有n个结点的完全二叉树的深度k为  log2(n+1) 上取整5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i 的结点有:若i>0,将 i 看作孩子节点 双亲序号:(i-1)/2; 将 i 看作父亲节点 且2i+1<n,左孩子序号:2i+1,                                   2i+2<n,右孩子序号:2i+2.1.4 二叉树的存储二叉树的存储结构分为:顺序存储和类似于链表的链式存储。顺序存储在下节介绍。二叉树的链式存储是通过一个一个的节点引用起来的,常见的表示方式有二叉和三叉表示方式,具体如下:// 孩子表示法class Node {int val; // 数据域Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树}// 孩子双亲表示法class Node {int val; // 数据域Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树Node parent; // 当前节点的根节点}一键获取完整项目代码孩子双亲表示法后序在平衡树位置介绍,本文采用孩子表示法来构建二叉树。1.5 二叉树的基本操作1.5.1 二叉树的简单创建在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。为了简单些, 此处手动创建一颗简单的二叉树. 先学习二叉树的操作, 后续再研究二叉树真正的创建方式.public class BinaryTree {    static class TreeNode {        public int val;        public TreeNode left;        public TreeNode right;         public TreeNode(int val) {            this.val = val;        }    }    public void createTree() {        TreeNode A = new TreeNode(4);        TreeNode B = new TreeNode(2);        TreeNode C = new TreeNode(7);        TreeNode D = new TreeNode(1);        TreeNode E = new TreeNode(3);        TreeNode F = new TreeNode(6);        TreeNode G = new TreeNode(9);        TreeNode H = new TreeNode('H');        A.left = B;        A.right = C;        B.left = D;        B.right = E;        C.left = F;        C.right = G;        E.right = H;       }}一键获取完整项目代码java再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:1. 空树2. 非空:根节点,根节点的左子树、根节点的右子树组成的。1.5.2 二叉树的遍历1. 前中后序遍历NLR:前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点--->根的左子树--->根的右树。LNR:中序遍历(Inorder Traversal)——根的左子树--->根节点--->根的右子树。LRN:后序遍历(Postorder Traversal)——根的左子树--->根的右子树--->根节点。下图是 前序遍历的递归过程,  中序遍历 和 后序遍历类似.前序遍历结果:1 2 3 4 5 6中序遍历结果:3 2 1 5 4 6后序遍历结果:3 1 5 6 4 1以上图二叉树为例, 代码实现前中后序递归遍历.注意:  根的左子树, 不只是根的左节点, 而是根左边的整颗子树. 右子树同理.  如上右图. 代码实现前分析(以前序遍历为例): 前序遍历的顺序为 根 左子树 右子树, 凡是递归必有一终止条件, 以上右图为例, 从根节点往左递 到 3 ,3的左子树为null, 则返回. 所以 终止条件为 root 等于null 则返回.按照顺序: 先打印根节点, 再递归左子树, 后递归右子树.  //前序遍历: 根 左 右    public void preOrder(TreeNode root) {        if(root == null) return;//终止条件        //前序遍历 先打印根.        System.out.print(root.val+" ");        //再处理左子树.        preOrder(root.left);        //最后处理右子树.        preOrder(root.right);    }//中序遍历: 左 根 右    public void inOrder(TreeNode root) {        if (root == null) return;        inOrder(root.left);        System.out.print(root.val + " ");        inOrder(root.right);    }//后序遍历: 左 右 根    public void postOrder(TreeNode root) {        if(root == null) return;        postOrder(root.left);        postOrder(root.right);        System.out.print(root.val+" ");    }一键获取完整项目代码java画图理解前序遍历的递归过程.  A的右子树递归过程与上图类似.2. 层序遍历层序遍历:除了前序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在 层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层 上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历, 如下图所示, 层序遍历的结果: A B C D E F G H 代码实现非递归 层序遍历 : 非递归层序遍历, 我们可以借助上一章所学的队列, 利用队列"先进先出" 的特点 , 实现层序遍历思路: 二叉树以 根节点  左节点  右节点 的顺序入队,  出队顺序即为层序遍历 如下图:具体步骤: 先将 根节点 入队 , 进入循环 while(!queue.isEmpty()) , 弹出栈顶元素 赋给 cur . 打印根节点,  将cur 的 左右节点入队. //二叉树的层序遍历    public void levelOrder1(TreeNode root) {        if(root == null) {            return;        }        Queue<TreeNode> queue = new LinkedList<>();        queue.offer(root);        while(!queue.isEmpty()) {            TreeNode cur = queue.poll();            System.out.print(cur.val+" ");            if(cur.left != null) {                queue.offer(cur.left);            }            if(cur.right != null) {                queue.offer(cur.right);            }        }    }一键获取完整项目代码java1.5.3 二叉树的基本操作//1. 获取树中节点的个数int size(Node root);第一种方法: 通过二叉树的遍历求, 每递归遍历一次, size++.public int size = 0;    //前序遍历,求二叉树节点个数    public void nodeSize(TreeNode root) {        if(root == null) return;        size++;        nodeSize(root.left);        nodeSize(root.right);    }    //中序和后序也类似, 无非就是把打印节点的代码换成size++;一键获取完整项目代码java第二种方法: 通过子问题解决: 总节点数 = 左子树 + 右子树 + 1;思路: 总的节点数实际上就是所有的左节点数 + 右节点数 + 1.public int nodeSize2(TreeNode root) {        if(root == null) return 0;        return nodeSize2(root.left) + nodeSize2(root.right) + 1;    }一键获取完整项目代码java//2. 获取叶子节点的个数int getLeafNodeCount(Node root);第一种思路: 定义leafSize存储叶子节点的个数,  在前序遍历中, 当某个节点的左右节点都为null时, 则leafSize++;public int leafSize = 0;    public void gerLeafSize(TreeNode root) {        if(root == null) return;         if(root.left == null && root.right == null) {            leafSize++;        }        gerLeafSize(root.left);        gerLeafSize(root.right);    }一键获取完整项目代码java第二种思路: 求二叉树叶子节点的个数 = 左子树叶子节点的个数 + 右子树叶子节点的个数.public int getLeafSize2(TreeNode root) {        if(root == null) return 0;        if(root.left == null && root.right == null) {             return 1;        }        return getLeafSize2(root.left)+                 getLeafSize2(root.right);    }一键获取完整项目代码java//3. 获取第K层节点的个数int getKLevelNodeCount(Node root,int k);//获取第K层节点的个数    public int getKLeveNodeCount(TreeNode root,int k) {        if(root == null) return 0;        if(k == 1) {            return 1;        }        return getKLeveNodeCount(root.left,k-1) +                getKLeveNodeCount(root.right,k-1);    }一键获取完整项目代码java//4. 获取二叉树的高度int getHeight(Node root);思路: 二叉树的高度 = 左子树与右子树之中最大高度+1,//求二叉树的高度.    public int gerHeight(TreeNode root) {        if(root == null) return 0;        if(root.left == null && root.right == null) {            return 1;        }        int leftTree = gerHeight(root.left);        int rightTree = gerHeight(root.right);        return (leftTree > rightTree ? leftTree+1 : rightTree+1);    }一键获取完整项目代码java// 5. 检测值为value的元素是否存在public boolean find(Node root, int val);还是一样的, 通过遍历来确定二叉树是否存在值为value的节点, 以前序遍历为例, 判断根节点,递归左子树, 在判断左子树, 递归右子树,再判断右子树. 如果全都没找到则return false.//检测值为val的元素是否存在    public boolean find(TreeNode root,char key) {        if(root == null) return false;        if(root.val == key) {            return true;        }        boolean leftVal = find(root.left,key);        if(leftVal == true) {            return true;        }        boolean rightVal = find(root.right,key);        if(rightVal == true) {            return true;        }        return false;    }一键获取完整项目代码java// 6. 判断一棵树是不是完全二叉树boolean isCompleteTree(Node root);思路: 利用层序遍历, 将节点入队, 当cur 遇到 null 之后, 如果后面还有不为空的节点就说明 该二叉树不是完全二叉树, 否则是.————————————————原文链接:https://blog.csdn.net/2302_81886858/article/details/143601871
  • [技术干货] Java携手HanLP拆解各省旅游宣传口号
    前言        “世界那么大,我想去看看。”当这句辞职信上的金句一夜之间刷爆朋友圈时,它不再只是一个人的冲动,而是整个时代的集体心跳。过去的十年,是中国文旅产业狂飙突进的十年:高铁织网、机场下沉、短视频种草、城市营销内卷,从“好客山东”到“有一种叫云南的生活”,从“诗画浙江”到“活力广东”,每一句口号都恨不得把山河日月打包塞进14亿人的备忘录。据文旅部统计,2023年国内出游人次已突破60亿,相当于每个中国人一年里平均旅行4.3次。当流量成为硬通货,各省市政府比任何时候都清楚:一句朗朗上口的旅游口号,就是一张价值千亿的“城市名片”。         然而,口号多了,故事却开始“撞衫”。也许你和我一样,在深夜刷手机时闪过一丝恍惚:为什么“七彩云南”和“多彩贵州”像孪生兄弟?为什么“诗画江南”和“水墨安徽”仿佛同一幅宣纸上的两笔淡墨?当“心灵栖息地”“诗和远方”被反复吟唱,我们不禁要问:到底是山河相似,还是创意枯竭?这场“撞脸”游戏背后,隐藏着一条看不见的赛道——语义相似度的暗战。谁能率先用算法拆穿“伪原创”,谁就能在下一次城市品牌洗牌中占得先机。         今天,就让我们一起按下“运行”按钮,看60亿出游人次沉淀下的千言万语,如何在0和1之间现出原形。或许当算法告诉我们““千山万水”不过是“万水千山”的回文时,我们会心一笑;也或许,当某句被忽视的小众口号在相似度榜单上孤独地远离集群,我们会重新发现那些被低估的远方。屏幕上的进度条一闪而过,输出的不只是冰冷的矩阵,而是一幅中国文旅的“语义星图”——哪里灯火通明,哪里尚待点亮,一目了然。文本将通过Java和HanLP对口号进行简单的相似性评估,我们要做的,不只是给口号“查重”,更是给每一座城市找到专属签名。让“撞脸”的归算法,让惊艳的归山河。 一、各省旅游口号        宣传口号对于各省的文旅,就相当于我们的姓名相当于个人,通过一个极富有意义的宣传口号,在人民群众中形成口口相传的文化符号,不仅对于文旅部门是一个积极的符号,对于经济也是一个积极的拉动。因此本节来简单阐述一下旅游口号的意义以及几个具体的实例。 1、旅游口号的意义        给每个省量身定制一句旅游口号,表面看只是“一句话工程”,实则是把区域竞争、经济转型、文化认同、流量博弈全部压缩进十几个字的“超级压缩包”。一句话背后,至少藏着七重意义。1. 注意力稀缺时代的“3 秒电梯广告”:省级目的地必须在 3 秒内让陌生游客产生“记忆钩子”——“好客山东”=豪爽人设+山东大汉;“诗画浙江”=水墨滤镜+江南古镇,口号是最小单位的注意力容器。2. 区域竞争的“顶级域名”:“多彩贵州”注册成商标后,贵州在央视、高铁、机场、短视频里统一输出“多彩”色系。3. 经济转型的“产业路由器”:黑龙江喊“北国好风光”,把夏季 20℃的平均气温包装成“天然空调房”;2023 年夏季游客量首超冬季,GDP 同比+11.4%。4. 文化认同的“二维码”:云南“有一种叫云南的生活”用一句话召唤了逃离内卷的情绪,2024 年小红书“云南生活”话题浏览破 120 亿次,带动 2.8 万外省年轻人旅居式移民。5. 投资招引的“前置招商手册”:“交响丝路,如意甘肃”把敦煌、张掖、酒泉打包成“丝路黄金段”。6. 流量算法的“SEO 关键词”:抖音热点词“水韵江苏”连续 19 个月占据华东 POI 播放量 TOP3,官方话题播放量 380 亿次。7. 危机公关的“情绪防弹衣”:“知音湖北,遇见无处不在”用情感修复策略,把“重疫区”重编码为“懂你的湖北”。 2、旅游口号示例        下面来看看全国各省的几个旅游宣传口号,看看你的家乡省份的宣传口是什么?数据来源于互联网,其中有一些可能有不一致,比如有的口号已经有了新口号,在此仅展示实例,数据不做较真,后续会根据当地文旅厅等网站进行核对。           图源来源:全国各省市旅游口号哪个最响亮?。 序号 省份名称 2023及以前 20251 北京市 东方古都,万里长城 魅力北京2 天津市 近代中国看天津 天天乐道,津津有味3 河北省 诚义燕赵,胜境河北 这么近,那么美,周末到河北4 山西 晋善晋美(2017前) 华夏古文明,山西好风光(17年后)5 内蒙古 祖国正北方,亮丽内蒙古 壮美内蒙古,亮丽风景线6 辽宁省 乐游辽宁,不虚此行 山海有情,天辽地宁7 吉林省 白山松水,豪爽吉林 白山松水,吉祥吉林8 黑龙江 北国好风光,尽在黑龙江 冰雪之冠,魅力黑龙江        篇幅有限,在此不一一列出,感情的大家可以在互联网搜索到。 二、当Java碰上HanLP        Java——这位在企业服务领域深耕二十余年的“老兵”,携手HanLP——国产自然语言处理界的“锋利手术刀”,决定下场做一回“文旅侦探”。Java的跨平台、高并发、生态成熟,让它成为政府、高校、咨询公司最趁手的开发语言;HanLP则凭借对中文语义的深度建模,能把一句口号切成词性、依存、语义角色,甚至投射到高维向量空间,让“像”与“不像”不再凭感觉,而是可计算、可对比、可排序的浮点数。当代码跑起来,每一条口号都不再只是广告屏上的金色字幕,而是向量空间里的一颗星辰——距离越近,“撞脸”越实锤。 1、HanLP的应用        HanLP 是面向中文的一站式的自然语言处理开源工具包,由何晗(hankcs)开发并维护。它既支持传统基于词典与规则的方法,也内置深度学习模型,涵盖分词、词性标注、命名实体识别、句法分析、文本分类、情感分析、关键词提取、自动摘要等常用 NLP 任务。HanLP 提供 Java 原生 API 和 Python 接口(pyhanlp),并可通过 REST 服务或本地模型两种方式调用,兼顾研究、教学与工业部署需求。其分词核心采用 双数组 Trie + 维特比 + 用户自定义词典 的混合策略,在保持速度的同时能灵活扩展词汇;深度学习模块则基于 Bi-LSTM-CRF、BERT 等结构,对歧义、未登录词具有更强容错能力。工具包内置 99 种语言模型、数十种领域词典,开箱即用,且全面兼容 Windows/Linux/Mac,Maven/Gradle 一键引入,是中文 NLP 入门与落地的首选之一。 2、程序时序调用        在Java中集成HanLP并且进行省级旅游宣传口号相似性的计算的程序时序调用流程如下图所示(其中比较重要的就是基于HanLP的分词和词向量计算):   3、关键实现步骤        根据以上的时序图,下面我们将对程序的实现逻辑进行详细的介绍。首先在Maven工程配置文件中引入HanLP的依赖引用,具体的HanLP版本大家可以根据实际情况选择: <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><dependencies><dependency><groupId>com.hankcs</groupId><artifactId>hanlp</artifactId><version>portable-1.8.4</version></dependency></dependencies></project>一键获取完整项目代码XML         第二步:旅游口号预处理,准备34个省份的旅游口号数据,存储在LinkedHashMap中。核心代码如下(旅游口号使用2025年的最新数据): // 省份旅游口号数据Map<String, String> slogans = new LinkedHashMap<>();slogans.put("北京", "魅力北京");slogans.put("上海", "这里是上海(This is Shanghai)");slogans.put("天津", "天天乐道,津津有味");slogans.put("重庆", "雄奇山水,新韵重庆");slogans.put("河北", "这么近,那么美,周末到河北");slogans.put("山西", "华夏古文明,山西好风光");slogans.put("内蒙古", "壮美内蒙古,亮丽风景线");slogans.put("辽宁", "山海有情,天辽地宁");slogans.put("吉林", "白山松水,吉祥吉林");slogans.put("黑龙江", "冰雪之冠,魅力黑龙江");slogans.put("江苏", "水韵江苏,诗意江南");slogans.put("浙江", "诗画江南,活力浙江");slogans.put("安徽", "美好安徽,迎客天下");slogans.put("福建", "清新福建,山海画廊");slogans.put("江西", "江西风景独好");slogans.put("山东", "好客山东");slogans.put("河南", "老家河南,山水太行");slogans.put("湖北", "千湖之省,灵秀湖北");slogans.put("湖南", "湘楚神韵,灵动湖南");slogans.put("广东", "粤见山海,不负热爱");slogans.put("广西", "秀甲天下,心仪广西");slogans.put("海南", "阳光海南,度假天堂");slogans.put("四川", "锦绣天府,安逸四川");slogans.put("贵州", "走遍大地神州,醉美多彩贵州");slogans.put("云南", "有一种叫云南的生活");slogans.put("西藏", "世界屋脊,心灵净土");slogans.put("陕西", "山水人文,大美陕西");slogans.put("甘肃", "交响丝路,如意甘肃");slogans.put("青海", "大美青海");slogans.put("宁夏", "塞上江南,神奇宁夏");slogans.put("新疆", "大美新疆,丝路风情");slogans.put("香港", "Hello,Hong Kong");slogans.put("澳门", "感受澳门");一键获取完整项目代码java         第三步:调用HanLP.segment()方法对每个口号进行分词处理,统计每个口号中每个词的出现频率,并将所有词存储在一个集合中。核心代码如下: // 获取所有口号的分词结果List<Map<String, Integer>> wordFrequencyList = new ArrayList<>();Set<String> allWords = new HashSet<>();for (String slogan : slogans.values()) {List<Term> terms = HanLP.segment(slogan);Map<String, Integer> wordFrequency = new HashMap<>();for (Term term : terms) {String word = term.word;wordFrequency.put(word, wordFrequency.getOrDefault(word, 0) + 1);allWords.add(word);}wordFrequencyList.add(wordFrequency);}一键获取完整项目代码java         第四、五步:分词向量计算,将每个口号的词频统计结果转换为向量。同时调用余弦相似度进行计算,返回相似度结果。 // 将每个口号转换为词频向量List<int[]> vectors = new ArrayList<>();List<String> allWordsList = new ArrayList<>(allWords);for (Map<String, Integer> wordFrequency : wordFrequencyList) {int[] vector = new int[allWordsList.size()];for (int i = 0; i < allWordsList.size(); i++) {String word = allWordsList.get(i);vector[i] = wordFrequency.getOrDefault(word, 0);}vectors.add(vector);} // 计算每个口号之间的余弦相似度,并存储结果List<SimilarityResult> similarityResults = new ArrayList<>();for (int i = 0; i < vectors.size(); i++) {for (int j = i + 1; j < vectors.size(); j++) {double similarity = cosineSimilarity(vectors.get(i), vectors.get(j));similarityResults.add(new SimilarityResult(slogans.keySet().toArray()[i].toString(),slogans.keySet().toArray()[j].toString(), similarity));}}一键获取完整项目代码java         计算余弦值的核心方法如下: private static double cosineSimilarity(int[] vector1, int[] vector2) {double dotProduct = 0.0;double normA = 0.0;double normB = 0.0;for (int i = 0; i < vector1.length; i++) {dotProduct += vector1[i] * vector2[i];normA += Math.pow(vector1[i], 2);normB += Math.pow(vector2[i], 2);}return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));}一键获取完整项目代码java         经过上面的计算后,将得到的结果存储到一个对象中,接收结果的对象如下: // 辅助类,用于存储相似度结果static class SimilarityResult {String province1;String province2;double similarity; SimilarityResult(String province1, String province2, double similarity) {this.province1 = province1;this.province2 = province2;this.similarity = similarity;}}一键获取完整项目代码java         第六步、为了方便将数据进行展示,我们将按照两个省份的口号相似性结果进行排名,相似度大的排名靠前。最后将经过排序后的结果进行输出,代码如下: // 按相似度从大到小排序similarityResults.sort((a, b) -> Double.compare(b.similarity, a.similarity)); // 输出结果for (SimilarityResult result : similarityResults) {System.out.printf("省份:%s 和 省份:%s 的相似度为:%.2f%n", result.province1, result.province2, result.similarity);}一键获取完整项目代码java三、成果展示        这是一场“让创意回到创意”的供给侧改革。我们设想这样的场景:某省文旅厅在敲定新口号前,把候选句子扔进系统,0.3秒内收到一份“相似度预警报告”——“与江西省2018年旧版口号相似度68%,存在舆情风险”;某高校广告系课堂里,学生用可视化面板直观看到“‘大美××’类表述在近五年使用率上升312%”,于是决定换个赛道;甚至,当AI写作工具疯狂输出“千篇一律”的文案时,我们的引擎能像论文查重一样,给原创性打出分数,让真正的金句脱颖而出。本节我们将实际展示这些 省份综合相似的口号。 1、综合对比        在IDE中运行以上程序,可以看到全国各个省份的旅游宣传口号的相似度情况,首先来看一下综合的对比情况:           比较有意思的是,在全国的各个省的旅游宣传口号中,确实存在一些比较有意思的口号,而且大家的相似度还挺好,当然,因为口号比较短,因此重复的概率还是比较高的。下面我们会根据不同的重复度进行一些对比。 2、口号相似度超40%        首先来看超过40%相似度的省份有哪些: 省份:陕西 和 省份:青海 的相似度为:0.47省份:陕西 和 省份:新疆 的相似度为:0.46省份:甘肃 和 省份:新疆 的相似度为:0.46省份:青海 和 省份:新疆 的相似度为:0.44省份:重庆 和 省份:河南 的相似度为:0.40省份:江苏 和 省份:浙江 的相似度为:0.40省份:江苏 和 省份:宁夏 的相似度为:0.40省份:浙江 和 省份:宁夏 的相似度为:0.40一键获取完整项目代码bash        为了方便展示,我们以表格的形式来展示上面的这些彼此心有灵犀的省份: 序号 省份 对比省份 相似度 对比口号1 陕西省 青海省 47% 山水人文,大美陕西  VS 大美青海2 陕西省 新疆自治区 46% 山水人文,大美陕西  VS 大美新疆,丝路风情3 甘肃省 新疆自治区 46% 交响丝路,如意甘肃  VS  大美新疆,丝路风情4 青海省 新疆自治区 44% 大美青海  VS 大美新疆,丝路风情5 重庆市 河南省 40% 雄奇山水,新韵重庆 VS 老家河南,山水太行6 江苏省 浙江省 40% 水韵江苏,诗意江南 VS 诗画江南,活力浙江7 江苏省 宁夏自治区 40% 水韵江苏,诗意江南 VS 塞上江南,神奇宁夏8 浙江省 宁夏自治区 40% 诗画江南,活力浙江 VS 塞上江南,神奇宁夏四、总结        以上就是本文的主要内容,文本将通过Java和HanLP对口号进行简单的相似性评估,我们要做的,不只是给口号“查重”,更是给每一座城市找到专属签名。让“撞脸”的归算法,让惊艳的归山河。博文不仅仅介绍了旅游宣传口号对各省的重要性,并且以2025年的最新宣传口号为例,使用Java语言,结合HanLP进行词向量计算,并且通过计算余弦相似性来求解两个省的旅游口号相似性,文章的最后给出了各个省的宣传口号相似性对比结果。以上内容来源于自主计算,受限于评价的口号字数较少和博主的能力所限,因此想要绝对的不重复比较苦难,得出的结果也是一家之言。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。————————————————原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/151933497
  • [技术干货] 【Java 开发日记】我们来说一下 Mybatis 的缓存机制
    核心概览一级缓存:默认开启,作用范围在 同一个 SqlSession 内。二级缓存:需要手动配置开启,作用范围在 同一个 Mapper 命名空间(即同一个 Mapper 接口)内,可以被多个 SqlSession 共享。一级缓存1. 作用域SqlSession 级别:当同一个 SqlSession 执行相同的 SQL 查询时,MyBatis 会优先从缓存中获取数据,而不是直接查询数据库。它是 默认开启 的,无法关闭,但可以配置其作用范围(SESSION 或 STATEMENT)。2. 工作机制第一次执行查询后,查询结果会被存储到 SqlSession 关联的一级缓存中。在同一个 SqlSession 中,再次执行 完全相同的 SQL 查询(包括语句和参数)时,会直接返回缓存中的对象,而不会去数据库查询。如果 SqlSession 执行了 增(INSERT)、删(DELETE)、改(UPDATE) 操作,或者调用了 commit()、close()、rollback() 方法,该 SqlSession 的一级缓存会被清空。这是为了防止读取到脏数据。3. 示例说明// 假设获取的 SqlSession 和 UserMappertry (SqlSession sqlSession = sqlSessionFactory.openSession()) {    UserMapper mapper = sqlSession.getMapper(UserMapper.class);     // 第一次查询,会发送 SQL 到数据库    User user1 = mapper.selectUserById(1L);    System.out.println(user1);     // 第二次查询,SQL 和参数完全相同,直接从一级缓存返回,不查询数据库    User user2 = mapper.selectUserById(1L);    System.out.println(user2);     // 判断是否为同一个对象(是,因为从缓存中返回的是同一个对象的引用)    System.out.println(user1 == user2); // 输出:true     // 执行一个更新操作    mapper.updateUser(user1);    // 此时,一级缓存被清空     // 第三次查询,因为缓存被清空,会再次发送 SQL 到数据库    User user3 = mapper.selectUserById(1L);    System.out.println(user3 == user1); // 输出:false (虽然是同一条数据,但已是新对象)}一键获取完整项目代码4. 注意事项对象相同:一级缓存返回的是 同一个对象的引用,因此在同一个 SqlSession 内,你操作的都是同一个 Java 对象。分布式环境:一级缓存无法在多个应用服务器之间共享,因为它绑定在单个请求的 SqlSession 上。二级缓存1. 作用域Mapper 级别 / Namespace 级别:多个 SqlSession 在访问同一个 Mapper 的查询时,可以共享其缓存。它是 默认关闭 的,需要在全局配置中开启,并在具体的 Mapper XML 中显式配置。2. 开启与配置a. 全局配置文件 (mybatis-config.xml):必须显式设置开启二级缓存(虽然默认是 true,但显式声明是个好习惯)。<configuration><settings><!-- 开启全局二级缓存,默认就是 true,但建议写明 --><setting name="cacheEnabled" value="true"/></settings></configuration>一键获取完整项目代码b. Mapper XML 文件:在需要开启二级缓存的 Mapper.xml 中添加 <cache/> 标签。<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.mapper.UserMapper"><!-- 开启本 Mapper 的二级缓存 --><cacheeviction="FIFO"flushInterval="60000"size="512"readOnly="true"/> <!-- 其他 SQL 定义 --><select id="selectUserById" parameterType="long" resultType="User" useCache="true">SELECT * FROM user WHERE id = #{id}</select></mapper>一键获取完整项目代码<cache/> 标签属性:eviction:缓存回收策略。LRU(默认):最近最少使用。FIFO:先进先出。SOFT:软引用,基于垃圾回收器状态和软引用规则移除。WEAK:弱引用,更积极地移除。flushInterval:缓存刷新间隔(毫秒),默认不清空。size:缓存存放多少元素。readOnly:是否为只读。true:返回相同的缓存对象实例,性能好,但不允许修改。false(默认):通过序列化返回缓存对象的拷贝,安全,性能稍差。3. 工作机制当一个 SqlSession 执行查询后,在关闭或提交时,其查询结果会被存入二级缓存。另一个 SqlSession 执行相同的查询时,会先从二级缓存中查找数据。如果找到,则直接返回,否则再去数据库查询。任何一个 SqlSession 执行了 增、删、改 操作并 commit() 后,会清空 整个对应 Mapper 的二级缓存,以保证数据一致性。4. 示例说明// 第一个 SqlSessiontry (SqlSession sqlSession1 = sqlSessionFactory.openSession()) {    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);    User user1 = mapper1.selectUserById(1L); // 查询数据库    sqlSession1.close(); // 关闭时,数据存入二级缓存} // 第二个 SqlSession(与第一个不同)try (SqlSession sqlSession2 = sqlSessionFactory.openSession()) {    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);    // 查询相同的 SQL,直接从二级缓存获取,不查询数据库    User user2 = mapper2.selectUserById(1L);} // 第三个 SqlSession,执行了更新try (SqlSession sqlSession3 = sqlSessionFactory.openSession()) {    UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);    User user = mapper3.selectUserById(1L);    user.setName("New Name");    mapper3.updateUser(user); // 执行更新    sqlSession3.commit(); // 提交时,清空 UserMapper 的二级缓存} // 第四个 SqlSessiontry (SqlSession sqlSession4 = sqlSessionFactory.openSession()) {    UserMapper mapper4 = sqlSession4.getMapper(UserMapper.class);    // 因为缓存已被清空,所以会再次查询数据库    User user4 = mapper4.selectUserById(1L);}一键获取完整项目代码5. 注意事项实体类序列化:如果二级缓存的 readOnly="false",那么对应的实体类必须实现 Serializable 接口。事务提交:只有在 SqlSession 执行 commit() 或 close() 时,数据才会从一级缓存转存到二级缓存。缓存粒度:二级缓存是 Mapper 级别的,有时会显得比较粗粒度。可以通过 <cache-ref> 让多个 Mapper 共享一个缓存,但不推荐,容易引起数据混乱。缓存顺序与总结当发起一个查询请求时,MyBatis 的缓存查询顺序是:先查二级缓存:查看当前 Mapper 的二级缓存中是否有数据。再查一级缓存:如果二级缓存没有,再查看当前 SqlSession 的一级缓存中是否有数据。最后查数据库:如果两级缓存都没有,才发送 SQL 语句到数据库执行查询。查询到的数据会 先存入一级缓存,在 SqlSession 关闭或提交时,再转存到二级缓存。特性一级缓存二级缓存作用域SqlSessionMapper (Namespace)默认状态开启关闭是否共享否,Session 独享是,跨 Session 共享清空时机UPDATE/INSERT/DELETE, commit(), close()同 Mapper 的 UPDATE/INSERT/DELETE + commit()使用建议查询多,修改少的数据适合使用二级缓存,如字典表、配置项。数据实时性要求高的场景(如交易、订单)应谨慎使用二级缓存,或者设置较短的刷新间隔。在分布式环境中,默认的二级缓存(基于内存)是无法共享的,需要集成 Redis、Ehcache 等第三方缓存中间件来替代。理解缓存机制有助于解决一些“诡异”的问题,比如在同一个事务中,先后查询和更新,但由于一级缓存的存在,后续查询可能看不到其他线程的更新。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/154612181
  • [技术干货] 不用再纠结K8s了!Azure Functions+Quarkus:Java无服务器代码的“光速“写法
    一、为什么无服务器是Java的"救星"?传统Java应用的痛点:启动慢:Spring Boot启动时间动辄10秒+,开发时频繁重启超折磨资源浪费:即使应用空闲,也要一直占用服务器资源运维复杂:需要管理服务器、负载均衡、自动伸缩…无服务器架构的"超能力":按需执行:只有当有请求时才启动,不执行时完全不消耗资源自动伸缩:请求量大时自动扩容,量小时自动缩容极致启动:Quarkus的"超快启动"特性,让Java应用启动快如闪电我曾经在一次技术分享会上说:“如果Spring Boot是’坦克’,那么Quarkus+Azure Functions就是’火箭’。” 现在,这个"火箭"就要带你飞起来了!二、环境准备:别让环境配置拖垮你的"光速"2.1 安装必要工具# 1. 安装Azure CLI (如果还没安装)# https://learn.microsoft.com/zh-cn/cli/azure/install-azure-cli# 2. 安装Maven (如果还没安装)# https://maven.apache.org/install.html# 3. 安装Quarkus CLI (可选,但推荐)# https://quarkus.io/guides/getting-started#install-the-quarkus-cli一键获取完整项目代码bash为什么需要这些工具?Azure CLI:管理Azure资源的命令行工具,比在网页上点点点快多了Maven:Java项目构建工具,管理依赖、编译、打包Quarkus CLI:提供快速创建Quarkus项目的便捷方式,避免手写繁琐配置我踩过的坑:在安装Azure CLI时,我直接跳过了验证步骤,结果在登录时发现"认证失败",浪费了20分钟。所以,一定要按文档步骤安装,别偷懒!2.2 创建Azure资源组# 创建Azure资源组 (替换yourResourceGroupName为你的资源组名)az group create --name yourResourceGroupName --location eastus一键获取完整项目代码bash关键参数解释:--name:资源组的名称,必须全局唯一--location:Azure区域,选择离你近的区域(如eastus、westus2等),能减少网络延迟为什么选eastus?因为我的客户主要在北美,选eastus能减少延迟。在实际项目中,一定要根据用户分布选择区域,别随便填"westus"。三、创建Quarkus项目:从"Hello World"到"光速API"3.1 用Quarkus CLI创建项目# 创建Quarkus项目 (替换your-project-name为你的项目名)quarkus create app io.quarkus:quarkus-azure-functions-demo --extension=azure-functions,funqy-http一键获取完整项目代码bash为什么选择这些扩展?azure-functions:Quarkus对Azure Functions的支持,让Quarkus应用能轻松部署到Azure Functionsfunqy-http:Quarkus的Funqy扩展,提供HTTP触发器支持,简化了HTTP函数的编写这个命令会生成一个包含必要依赖和结构的项目。别担心,它会自动处理所有配置,比手写pom.xml快多了!3.2 项目结构详解生成的项目结构如下:quarkus-azure-functions-demo/├── src│   ├── main│   │   ├── java│   │   │   └── io│   │   │       └── quarkus│   │   │           ├── GreetingFunction.java   # 函数入口│   │   │           └── GreetingService.java    # 依赖注入服务│   │   └── resources│   │       └── application.properties          # 配置文件├── pom.xml                                     # 项目依赖配置└── README.md一键获取完整项目代码为什么这个结构这么重要?GreetingFunction.java:这是你的函数入口,Azure Functions会调用这里的@Funq方法GreetingService.java:实现业务逻辑的服务类,通过CDI注入到函数中application.properties:配置文件,设置Azure Functions的参数四、代码实现:从"Hello World"到"光速API"的深度解析4.1 创建GreetingService.javapackage io.quarkus;import javax.enterprise.context.ApplicationScoped;/** * 业务服务类:实现欢迎消息的生成逻辑 *  * @ApplicationScoped:CDI作用域注解,表示该bean在应用范围内单例存在 * 为什么用ApplicationScoped?因为它会在应用启动时创建一次,之后所有请求共享同一个实例 * 这比每次请求都创建新实例要高效得多,特别适合无服务器环境 */@ApplicationScopedpublic class GreetingService {    /**     * 生成欢迎消息     *      * @param name 用户名     * @return 包含用户名的欢迎消息     */    public String greeting(String name) {        return "Welcome to build Serverless Java with Quarkus on Azure Functions, " + name;    }}一键获取完整项目代码java为什么这个服务类这么简单?无服务器架构的核心思想是"函数化",每个函数应该只做一件事这个服务类只负责生成欢迎消息,不涉及任何HTTP或Azure相关逻辑这种解耦让代码更清晰,也更容易测试4.2 创建GreetingFunction.javapackage io.quarkus;import javax.inject.Inject;import io.quarkus.funqy.Funq;/** * Azure Functions函数入口 *  * @Funq:Quarkus Funqy的注解,表示这个方法将作为函数触发器 *  * 为什么用Funqy?因为Funqy是Quarkus的函数式编程扩展,专为无服务器设计 * 它比传统的Spring Cloud Function更轻量、启动更快 */public class GreetingFunction {    /**     * 依赖注入:将GreetingService注入到函数中     *      * @Inject:CDI的依赖注入注解,自动注入GreetingService实例     * 为什么用依赖注入?因为它让代码更解耦,更容易测试     */    @Inject    GreetingService gService;    /**     * HTTP触发器:处理HTTP请求的函数     *      * @Funq:Funqy的注解,表示这个方法将作为函数触发器     * @param name 从请求参数中获取的用户名     * @return 生成的欢迎消息     */    @Funq    public String greeting(String name) {        // 调用服务类生成欢迎消息        return gService.greeting(name);    }    /**     * 测试函数:用于验证函数是否正常工作     *      * @Funq:Funqy的注解,表示这个方法将作为函数触发器     * @return 固定的测试消息     */    @Funq    public String funqyHello() {        return "hello funqy";    }}一键获取完整项目代码java为什么这个函数这么简洁?没有Spring Boot的@RestController,没有@RequestMapping,没有@Autowired只用@Funq和@Inject,让函数更专注于业务逻辑这种简洁性是无服务器架构的核心价值:只关注你要做的事情,不关注框架细节我踩过的坑:第一次写这个函数时,我错误地用了@GetMapping,结果函数无法触发。后来才发现,Azure Functions需要的是@Funq,不是Spring的注解。别像我一样,多踩坑!4.3 配置application.properties# Azure Functions配置azure.functions.name=quarkus-azure-functions-demoazure.functions.runtime=javaazure.functions.memory=128 # MBazure.functions.timeout=30 # 秒# Quarkus Funqy配置funqy.http.enabled=truefunqy.http.path=/api一键获取完整项目代码properties关键配置项详解:azure.functions.name:Azure Functions的名称,必须全局唯一azure.functions.runtime:运行时,这里指定为Javaazure.functions.memory:分配给函数的内存,根据应用需求调整azure.functions.timeout:函数执行超时时间,单位秒funqy.http.enabled:启用HTTP触发器funqy.http.path:HTTP路径前缀,所有函数将通过这个路径访问为什么设置azure.functions.memory=128?因为我的应用很简单,128MB足够。如果应用需要更多内存,可以适当增加。别像我一样一开始设成1024,结果浪费了钱!五、本地测试:让"光速"在你眼前发生5.1 启动Quarkus开发模式mvn quarkus:dev一键获取完整项目代码bash为什么用mvarkus:dev?quarkus:dev是Quarkus的开发模式,支持热重载每次修改代码,Quarkus会自动重新编译并重新加载应用这比传统Java应用的重启快得多,简直是"开发神器"输出示例:INFO  [io.quarkus] (Quarkus Main Thread) quarkus-azure-function 1.0-SNAPSHOT on JVM (powered by Quarkus 2.15.0.Final.) started in 0.890s. Listening on: http://localhost:8080INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, funqy-http, smallrye-context-propagation, vertx]一键获取完整项目代码关键信息解读:started in 0.890s:启动时间仅0.89秒!这就是Quarkus的"超快启动"能力Profile dev activated:开发模式已激活Live Coding activated:热重载已激活,修改代码后自动生效5.2 测试HTTP函数# 测试greeting函数curl -d '"Dan"' -X POST http://localhost:8080/api/greeting# 测试funqyHello函数curl http://localhost:8080/api/funqyHello一键获取完整项目代码bash预期输出:"Welcome to build Serverless Java with Quarkus on Azure Functions, Dan""hello funqy"一键获取完整项目代码为什么用curl测试?curl是命令行HTTP测试工具,简单直接比在浏览器中输入URL更高效,特别适合自动化测试这是开发人员的"标配",别用浏览器测,浪费时间!我踩过的坑:第一次测试时,我错误地用了http://localhost:8080/greeting,结果返回404。后来才发现,路径前缀是/api,所以正确路径是/api/greeting。别像我一样,多走弯路!六、部署到Azure:从本地到云端的"光速"之旅6.1 部署命令mvn clean install -DskipTests -DtenantId=<your tenantId from shown previously> -DresourceGroup=yourResourceGroupName azure-functions:deploy一键获取完整项目代码bash1关键参数解释:clean install:清理并构建项目-DskipTests:跳过测试,加快构建速度-DtenantId:你的Azure AD租户ID,从之前登录Azure时的输出中获取-DresourceGroup:Azure资源组名称azure-functions:deploy:Maven插件的部署目标为什么需要-DtenantId?因为Azure Functions需要知道你的Azure AD租户,才能正确部署应用。别像我一样忘记这个参数,结果部署失败。6.2 部署成功后的输出[INFO] --- azure-functions-maven-plugin:1.17.0:deploy (default-cli) @ quarkus-azure-functions-demo ---[INFO] Using Azure Functions Core Tools version 4.0.4915[INFO] Uploading files to Azure Functions...[INFO] Deployed successfully.[INFO] Function URL: https://quarkus-azure-functions-demo.azurewebsites.net/api/greeting一键获取完整项目代码关键信息解读:Function URL:部署后的函数URL,这是你调用函数的地址https://quarkus-azure-functions-demo.azurewebsites.net/api/greeting:这是你的函数的完整URL6.3 测试部署后的函数curl -d '"Alice"' -X POST https://quarkus-azure-functions-demo.azurewebsites.net/api/greeting一键获取完整项目代码bash1预期输出:"Welcome to build Serverless Java with Quarkus on Azure Functions, Alice"一键获取完整项目代码1为什么部署后要测试?确认部署成功检查网络连接验证函数逻辑我踩过的坑:第一次部署后,我直接在浏览器中测试,结果返回"403 Forbidden"。后来才发现,Azure Functions默认是匿名访问,需要在Azure门户中配置HTTP触发器的权限。别像我一样,多花时间排查!七、无服务器架构的"超能力":为什么选择Azure Functions+Quarkus?7.1 无服务器架构的"光速"优势传统Java应用    Azure Functions+Quarkus启动时间:10+秒    启动时间:0.8+秒资源占用:持续占用    资源占用:按需分配部署:手动打包、上传    部署:一键命令行部署扩容:手动配置    扩容:自动处理代码复杂度:高    代码复杂度:低为什么这个对比这么重要?这不是简单的性能提升,而是架构的"降维打击"从"需要管理服务器"到"完全不用管服务器"从"启动慢、资源浪费"到"启动快、按需分配"7.2 为什么选择Quarkus?Quarkus是为云原生和无服务器设计的Java框架,它的优势包括:超快启动:通过提前编译和优化,启动时间从秒级降到毫秒级低内存占用:相比传统Java应用,内存占用减少50%+开发模式:热重载,开发体验极佳云原生支持:内置对Kubernetes、OpenShift等云平台的支持我在一次技术分享会上说:“如果Spring Boot是’大卡车’,那么Quarkus就是’跑车’。” 无服务器架构就是让这辆跑车在云端飞驰!八、结语:告别臃肿,拥抱"光速"无服务器通过这篇文章,我们成功用Azure Functions + Quarkus构建了一个无服务器Java应用。启动时间从10+秒降到0.8秒,资源占用大幅减少,部署变得无比简单。为什么说这是"光速"?启动快:0.8秒启动,比传统Java应用快10倍+部署快:一键命令行部署,比传统部署快10倍+开发快:热重载,修改代码后自动生效,比传统开发快10倍+最后的小提醒:在实际项目中,不要只关注启动速度,还要考虑函数的执行时间、内存占用和成本。别像我一样,一开始只关注启动速度,结果发现函数执行时间太长,导致成本飙升。无服务器不是免费的,要合理规划!现在,你已经掌握了Azure Functions + Quarkus的全部技能,快去给你的Java应用装上这个"光速"吧!记住,别再让Spring Boot的臃肿拖垮你的应用了,无服务器才是Java的未来!————————————————原文链接:https://blog.csdn.net/2401_88677290/article/details/151624628
  • [技术干货] 用了几年 Spring Boot,你真的知道请求是怎么进来的吗?—— JDK 原生实现 HTTP 服务
    一、你有没有真正理解过:一个 HTTP 请求是怎么“飞”到你的代码里的?我们每天都会写的代码: @RestControllerpublic class HelloController {    @GetMapping("/hello")    public String hello(@RequestParam String name) {        return "Hello, " + name;    }}一键获取完整项目代码java 启动后,浏览器访问 http://localhost:8080/hello?name=张三,立刻返回结果。 但你有没有想过: 客户端发出的请求,是怎么精准到达服务器的 8080 端口?服务器收到一堆字节流后,怎么知道要调用你的哪个方法?响应又是什么时候、怎么写回去的?Spring Boot 隐藏了太多细节,让我们误以为“写注解 = 有服务”。 今天,我们扔掉所有框架,只用 JDK 自带的 API,亲手实现一个真正的 HTTP 服务。 你会发现:一切都没有魔法,只有清晰的协议与流程。 二、Spring Boot 为什么能监听和处理请求为了对比,我们先用最简洁的方式说清楚 Spring Boot 的原理。 Spring Boot 启动时会自动创建一个嵌入式 Tomcat 实例,并绑定指定端口(默认 8080)。#比如:server:port: 8080一键获取完整项目代码yaml Tomcat 内部基于 Java 的 ServerSocket 监听 TCP 连接。所有 HTTP 请求到达后,Tomcat 解析成 ServletRequest/ServletResponse,转发给 Spring MVC 的 DispatcherServlet。DispatcherServlet 根据注解(如 @RequestMapping)找到对应方法,执行后把返回值序列化成 JSON 写回响应。整个过程我们几乎没写一行网络代码,却能提供服务。这很强大,但也容易让我们对底层产生“黑盒”感。 现在,我们把所有框架都扔掉——不用 Spring、不用 Tomcat、不用任何第三方库,只用 JDK 自带的 API,来实现一个完整的 HTTP 服务。 三、使用 JDK 自带的 HttpServer实现一个可运行的 HTTP 服务从 Java 6 起,JDK 提供了 com.sun.net.httpserver.HttpServer,这是一个轻量级、纯 Java 实现的嵌入式 HTTP 服务器。代码极简,却已经能完整处理请求和响应。 public class MyServer {    public static void main(String[] args) throws IOException {        //监听8080端口        HttpServer server = HttpServer.create(new InetSocketAddress(8080),0);         //创建一个HttpHandler        HttpHandler handler = new MiniHandler();         //如果有请求,就交给handler        server.createContext("/helloHttp",handler);        //启动服务器        server.start();        System.out.println("服务器启动成功");    }}  public class MiniHandler implements HttpHandler {    @Override    public void handle(HttpExchange exchange) throws IOException {         //1.获取URL的参数        String query= exchange.getRequestURI().getQuery();        //拿到第一个参数        String name=query.split("name=")[1];        //2.以json格式返回        String response = "{ \"code\": 200, \"message\": \"OK\", \"data\": \"Hello, " + name + "!\" }";         //3.发送回复        exchange.getResponseHeaders().set("Content-Type","application/json;charset=utf-8");;        exchange.sendResponseHeaders(200, response.length());        OutputStream os=exchange.getResponseBody();        os.write(response.getBytes(StandardCharsets.UTF_8));        os.close();    }} 一键获取完整项目代码java 运行这个 main 方法,然后打开浏览器访问http://localhost:8080/helloHttp?name=Http。 你会看到:  客户端(浏览器)发起 TCP 连接到你的机器 8080 端口。JDK 的 HttpServer 接受连接,解析 HTTP 请求行、头、查询参数。根据路径匹配到对应的HttpHandler。在 handle 方法里,你可以自由读取请求信息(方法、路径、参数、头、body)。你手动设置状态码、响应头、内容长度,然后通过 getResponseBody() 写入字节。底层自动把响应通过 Socket 发回客户端,连接关闭(或保持长连接)。Spring Boot 没有创造新东西,它只是把重复、易错的底层操作封装成了优雅的 API 四、结语:到这里,我们已经看清了 HTTP 请求如何通过 JDK 原生 API 被处理。 但你有没有想过:HttpServer背后又是谁在监听端口、收发字节?答案是:Socket。 HTTP 是一个应用层协议,它依赖于传输层的 TCP 协议进行可靠数据传输,而 TCP 连接在操作系统层面是通过 Socket API 来建立和管理的,下一篇,我们将彻底剥开最后一层封装,用最原始的 ServerSocket 和 Socket,从零实现一个能跑通的 HTTP 服务 —— 亲眼看看 TCP 连接是如何建立的,HTTP 报文是如何被一字节一字节解析的。————————————————原文链接:https://blog.csdn.net/2402_89042144/article/details/156026336
  • [技术干货] Docker 拉取部署 OpenJDK 全指南:替代方案、实操步骤与最佳实践
    OpenJDK 作为 Java SE 的开源实现,是企业级 Java 应用的核心运行环境,而 Docker 的容器化部署能有效解决环境一致性、资源隔离等问题。需要注意的是,官方 library/openjdk 镜像已正式弃用,仅保留早期访问版(Early Access builds)更新,生产环境需优先选择 amazoncorretto、eclipse-temurin 等替代方案。本文将详细介绍 Docker 环境搭建、OpenJDK 拉取部署步骤,并梳理关键注意事项、最佳实践及核心资源汇总。一、准备工作:搭建 Docker 环境容器化部署 OpenJDK 需依赖 Docker 环境,以下一键脚本支持主流 Linux 发行版(Ubuntu、CentOS、Debian),可快速完成 Docker、Docker Compose 安装及镜像访问支持配置。1.1 一键安装 Docker + Docker Compose + 轩辕镜像访问支持该脚本会自动完成三项核心操作,无需手动分步配置:安装最新版 Docker Engine 与 Docker Compose,满足容器构建与运行需求;配置轩辕镜像访问支持源,大幅提升 OpenJDK 镜像拉取访问表现;自动启动 Docker 服务并设置开机自启,确保环境长期可用。执行命令(复制到 Linux 终端直接运行):# 一键安装脚本(自动适配系统,无需修改参数)bash <(wget -qO- https://xuanyuan.cloud/docker.sh)一键获取完整项目代码验证环境:脚本执行完成后,运行以下命令确认 Docker 正常启动:# 查看Docker版本,确认安装成功docker --version # 查看Docker Compose版本,确认组件完整docker compose version一键获取完整项目代码二、Docker 拉取与部署 OpenJDK 的核心步骤部署前需先明确:官方 library/openjdk 已不适用于生产,需从替代镜像列表中选择(如 eclipse-temurin 跨平台兼容性强、amazoncorretto 免费长期支持、ibm-semeru-runtimes 低内存占用)。以下步骤以使用最广泛的 eclipse-temurin 为例,其他替代镜像的操作逻辑一致。2.1 步骤1:选择并拉取合适的 OpenJDK 镜像首先根据 Java 版本(优先 LTS 版)、基础系统(Ubuntu/Alpine)、功能需求(JDK/JRE)选择镜像标签,常见标签格式与拉取命令如下:需求场景    推荐镜像标签    拉取命令生产运行 JAR 包(Ubuntu)    eclipse-temurin:21-jre-ubuntu-jammy    docker pull docker.xuanyuan.run/eclipse-temurin:21-jre-ubuntu-jammy开发编译(Alpine 轻量)    eclipse-temurin:17-jdk-alpine3.22    docker pull docker.xuanyuan.run/eclipse-temurin:17-jdk-alpine3.22最新 LTS 版(默认 Ubuntu)    eclipse-temurin:latest    docker pull docker.xuanyuan.run/eclipse-temurin:latest开发编译(Ubuntu)    eclipse-temurin:11-jdk-ubuntu-jammy    docker pull docker.xuanyuan.run/eclipse-temurin:11-jdk-ubuntu-jammy轻量运行 JAR 包(Alpine)    eclipse-temurin:21-jre-alpine3.22    docker pull docker.xuanyuan.run/eclipse-temurin:21-jre-alpine3.22标签说明:21/17/11 为 Java LTS 版本,jre 表示仅运行时(无编译器),jdk 含编译器与调试工具,ubuntu-jammy/alpine3.22 为基础系统版本。2.2 步骤2:直接拉取镜像快速使用无需构建 Dockerfile 时,可直接通过容器执行 Java 命令(如查看版本、编译单个文件),适合临时测试场景:验证 Java 环境:拉取镜像后运行 java -version,确认环境正常# 运行后自动删除容器(--rm),输出Java版本信息docker run --rm eclipse-temurin:21-jre java -version一键获取完整项目代码正常输出示例:openjdk version "21.0.8" 2024-07-16 LTSEclipse Temurin Runtime Environment (build 21.0.8+9-LTS)OpenJDK 64-Bit Server VM (build 21.0.8+9-LTS, mixed mode)一键获取完整项目代码编译并运行单个 Java 文件:挂载本地目录到容器,直接编译 HelloWorld.java# 本地创建HelloWorld.java,内容为基础Java程序echo 'public class HelloWorld { public static void main(String[] args) { System.out.println("Hello Docker OpenJDK!"); } }' > HelloWorld.java # 挂载当前目录($PWD)到容器的/src,设置工作目录为/src,编译并运行docker run --rm -v $PWD:/src -w /src eclipse-temurin:21-jdk sh -c "javac HelloWorld.java && java HelloWorld"一键获取完整项目代码运行成功后,终端会输出 Hello Docker OpenJDK!,本地目录会生成 HelloWorld.class 编译文件。2.3 步骤3:通过 Dockerfile 构建部署应用生产环境需将应用与 OpenJDK 镜像打包,确保环境一致性,以下为两种常见构建场景:场景A:基础构建(直接运行已编译 JAR 包)适用于已有预编译 JAR 包的场景(如 Spring Boot 项目打包后的 app.jar),Dockerfile 示例:# 基础镜像:Java 21 LTS JRE(Ubuntu基础,兼容性强,适合生产环境)FROM eclipse-temurin:21.0.8-jre-ubuntu-jammy # 创建应用目录,避免权限冲突(使用镜像默认非root用户1001)RUN mkdir -p /opt/app && chown -R 1001:1001 /opt/appUSER 1001 # 复制本地JAR包到容器(--chown确保非root用户有权限读取)COPY --chown=1001:1001 app.jar /opt/app/ # 配置JVM参数:限制最大堆内存为512MB,避免容器内存溢出ENV JAVA_OPTS="-Xmx512m -XX:+UseContainerSupport" # 启动命令:通过环境变量注入JVM参数CMD ["sh", "-c", "java $JAVA_OPTS -jar /opt/app/app.jar"]一键获取完整项目代码构建并运行容器:# 构建镜像(标签为my-java-app,`.`表示当前目录为构建上下文)docker build -t my-java-app . # 后台运行容器,映射主机8080端口到容器8080端口(应用默认端口)docker run -d -p 8080:8080 --name my-app-container my-java-app # 验证容器是否正常启动docker ps | grep my-app-container一键获取完整项目代码场景B:多阶段构建(减小镜像体积)若需编译源码(如本地有 Java 源码或 Maven/Gradle 项目),可通过“多阶段构建”分离“编译阶段”与“运行阶段”,仅保留运行时依赖,大幅减小最终镜像体积(比基础构建小 50% 以上):# 阶段1:编译阶段(使用JDK编译源码,仅保留编译结果)FROM eclipse-temurin:21-jdk-alpine3.22 AS build-stage# 设置工作目录WORKDIR /src# 复制源码与构建配置文件(如pom.xml、src目录)COPY pom.xml ./COPY src ./src# 安装Maven(Alpine基础镜像需手动安装)并编译源码RUN apk add --no-cache maven && mvn clean package -DskipTests # 阶段2:运行阶段(仅使用JRE,移除编译器与构建工具)FROM eclipse-temurin:21-jre-alpine3.22WORKDIR /opt/app# 从编译阶段复制编译好的JAR包(仅保留target目录下的JAR)COPY --from=build-stage /src/target/app.jar ./ # 启动命令:适配Alpine轻量环境CMD ["java", "-Xmx512m", "-jar", "app.jar"]一键获取完整项目代码构建命令与场景 A 一致,最终镜像体积可从数百 MB 缩减至数十 MB,适合资源受限场景(如边缘节点、轻量容器集群)。三、部署 OpenJDK 镜像的关键注意事项3.1 必须替换弃用的官方镜像library/openjdk 已正式弃用,仅 2022 年 7 月后保留“早期访问版”(供测试新功能用),生产环境严禁使用,需替换为以下官方推荐替代镜像:amazoncorretto :AWS 维护,免费长期支持,适配 AWS 云环境;eclipse-temurin :Eclipse Adoptium 项目,跨平台兼容性最强,支持 Windows/Linux/macOS,企业级首选;ibm-semeru-runtimes :IBM 基于 OpenJ9 JVM,低内存占用(比传统 HotSpot JVM 省 30% 内存),适合微服务;sapmachine :SAP 维护,适配 SAP 系统(如 S/4HANA),支持 Cloud Foundry 云平台。3.2 生产环境优先选择 LTS 版本Java 版本分为“长期支持版(LTS)”和“非 LTS 版”,生产环境必须选择 LTS 版,避免短周期支持导致的安全补丁中断风险:推荐 LTS 版本:8、11、17、21(支持期限以发行商官方支持策略为准,如 Eclipse Adoptium / Amazon Corretto);避免非 LTS 版本:24、25(支持周期仅 6 个月,仅适合本地测试新功能)。3.3 适配宿主机架构,避免运行异常OpenJDK 替代镜像均支持多架构,需确保镜像架构与宿主机一致,否则会出现“exec format error”等启动失败问题:常见架构匹配:x86-64 服务器选 amd64 架构,ARM 服务器(如 AWS Graviton、阿里云 ARM 实例)选 arm64v8 架构;无需手动指定:Docker 会自动检测宿主机架构,拉取对应版本的镜像(如在 ARM 服务器上拉取 eclipse-temurin:21-jre,会自动获取 arm64v8 版本)。3.4 基础镜像选择:Ubuntu vs Alpine不同基础镜像的 libc 库不同,需根据应用兼容性选择:Ubuntu 基础(glibc):兼容性强,支持所有依赖 glibc 的 Java 库(如生成 PDF 的 iText、图片处理的 ImageIO),适合大多数企业应用;Alpine 基础(musl):体积轻量(基础镜像仅约 5MB),但部分依赖 glibc 的 JNI/native 库可能报错(如 PDF 处理、图片渲染、字体相关库),需通过 apk add libc6-compat 安装兼容库解决。3.5 非 root 用户运行,降低安全风险默认容器以 root 用户运行,若应用被入侵可能导致主机权限泄露,需强制使用非 root 用户:优先选自带非 root 用户的镜像:eclipse-temurin 默认含 1001 用户,amazoncorretto 含 sapmachine 用户,可直接通过 USER 指令切换;手动创建非 root 用户(若镜像无默认非 root 用户):# 在Dockerfile中添加以下指令RUN addgroup -S app-group && adduser -S app-user -G app-groupUSER app-user一键获取完整项目代码补充说明:使用固定 UID(如 1001)有助于在挂载宿主机目录时避免权限不一致问题。3.6 JVM 容器资源感知的生效前提Java 10+ 开始支持容器资源感知,Java 11+ 默认启用该特性(在 cgroup 正常生效的前提下),可自动适配容器的 CPU 核心数与内存限制;但在极老内核或特殊容器运行时环境中,cgroup 可能无法正常暴露,导致该特性失效,需手动确认环境兼容性。四、OpenJDK 容器化的最佳实践4.1 按需选择镜像变体,避免资源浪费OpenJDK 镜像提供多种变体,需根据场景精准选择:按功能选:仅运行 JAR 包选 JRE(无编译器,体积小);需编译源码或调试选 JDK;服务器端无 GUI 需求选 headless 版(如 21-jre-headless,移除 AWT/Swing 等 GUI 库);按基础系统选:兼容性优先选 Ubuntu,资源受限选 Alpine。4.2 优化 JVM 参数,适配容器资源JVM 默认可能误判容器资源(如读取主机 CPU/内存),需通过参数优化:Linux 容器:Java 8u191+、Java 11+ 默认启用 XX:+UseContainerSupport(cgroup 正常生效时),自动适配容器资源;通用参数配置:限制最大堆内存:-Xmx512m(建议设为容器内存的 50%-70%,如容器内存 1GB 则设 Xmx700m);固定初始堆内存:-Xms512m(与 Xmx 一致,减少内存波动);禁用 JVM GUI 相关功能:-Djava.awt.headless=true(在 headless 变体中已默认启用)。4.3 容器资源限制与 JVM 参数联动配置生产环境需同时限制容器资源与 JVM 堆内存,避免 OOM 风险,示例命令:docker run -d \  --memory=1g \  # 限制容器最大内存为1GB  --cpus=1.5 \   # 限制容器最大CPU核心数为1.5  -e JAVA_OPTS="-Xms512m -Xmx700m" \  # 堆内存设为容器内存的70%  -p 8080:8080 \  --name my-app-container \  my-java-app一键获取完整项目代码说明:在 Kubernetes 环境中,应同时配置 Pod 的 resources.requests/limits 与 JVM 堆参数,避免 OOMKilled。4.4 利用类数据共享(CDS),优化多容器部署部分镜像(如 ibm-semeru-runtimes 基于 OpenJ9 JVM)支持“类数据共享(CDS)”,多容器共享 JVM 类缓存,降低内存占用与启动时间:# 基于ibm-semeru-runtimes镜像启用CDS(仅适用于OpenJ9,不适用于HotSpot JVM)FROM ibm-semeru-runtimes:open-21-jre# 创建类缓存目录,赋予非root用户权限RUN mkdir -p /opt/shareclasses && chown 1001:1001 /opt/shareclassesUSER 1001COPY app.jar /opt/app/# 启用CDS,指定缓存目录CMD ["java", "-Xshareclasses:cacheDir=/opt/shareclasses", "-Xmx512m", "-jar", "/opt/app/app.jar"]一键获取完整项目代码效果:第二个及后续容器启动时间缩短 30%+,每个容器内存占用减少 20%+(需通过数据卷共享 /opt/shareclasses 目录)。4.5 定期更新镜像+安全扫描,保障稳定性OpenJDK 镜像会定期修复安全漏洞(如 Log4j、序列化漏洞),需建立常态化维护机制:定期拉取最新镜像:如每月执行 docker pull eclipse-temurin:21.0.8-jre-ubuntu-jammy,获取最新安全补丁;镜像安全扫描:使用 Trivy 工具检查漏洞,命令如下:# 安装Trivy(Alpine系统)apk add --no-cache trivy# 扫描镜像漏洞trivy image my-java-app一键获取完整项目代码发现高风险漏洞时,需及时更新基础镜像或应用依赖。4.6 避免依赖“latest”标签,锁定版本一致性latest 标签会自动指向镜像的最新版本,可能导致不同节点部署的 Java 版本不一致(如今天拉取是 21.0.8,明天可能变为 21.0.9),生产环境需:指定具体版本标签:如 eclipse-temurin:21.0.8-jre-ubuntu-jammy,而非 eclipse-temurin:21-jre;将标签写入配置文件:如 K8s 的 deployment.yaml、Docker Compose 的 docker-compose.yml,避免手动输入错误。五、核心资源汇总:命令、模板与问题排查5.1 核心命令速查操作场景    命令示例    说明拉取 OpenJDK 镜像    docker pull eclipse-temurin:21.0.8-jre    拉取 Java 21.0.8 LTS JRE 镜像验证 Java 版本    docker run --rm 镜像名 java -version    临时运行容器,输出版本后自动删除构建镜像    docker build -t 镜像标签 .    基于当前目录 Dockerfile 构建镜像后台运行容器(带资源限制)    docker run -d -p 8080:8080 --memory=1g --cpus=1.5 容器名    映射端口+限制资源,后台启动容器查看容器日志    docker logs -f 容器名    实时查看容器运行日志(排查启动失败问题)进入运行中容器    docker exec -it 容器名 /bin/bash    交互式进入容器终端(Ubuntu 基础)停止并删除容器    docker stop 容器名 && docker rm 容器名    停止容器后删除,避免残留资源镜像安全扫描    trivy image 镜像名    检查镜像中的安全漏洞5.2 Dockerfile 场景化模板模板1:生产环境基础部署(Ubuntu+JRE+非 root 用户)# 基础镜像:锁定Java 21.0.8 LTS JRE,Ubuntu Jammy基础FROM eclipse-temurin:21.0.8-jre-ubuntu-jammy # 创建应用目录,切换非root用户(固定UID 1001,避免挂载目录权限冲突)RUN mkdir -p /opt/app && chown -R 1001:1001 /opt/appUSER 1001 # 复制JAR包(确保本地JAR包名为app.jar)COPY --chown=1001:1001 app.jar /opt/app/ # JVM参数:适配容器资源,启用垃圾回收日志(便于排查内存问题)ENV JAVA_OPTS="-Xmx512m -Xms512m -XX:+UseContainerSupport -Xlog:gc*:file=/opt/app/gc.log:time,level,tags:filecount=5,filesize=100m" # 启动命令CMD ["sh", "-c", "java $JAVA_OPTS -jar /opt/app/app.jar"]一键获取完整项目代码模板2:轻量部署(Alpine+JRE-headless)# 基础镜像:Java 17 LTS JRE-headless,Alpine 3.22基础(体积轻量)FROM eclipse-temurin:17.0.16-jre-headless-alpine3.22 # 解决Alpine musl libc兼容性问题(适配JNI/native依赖库)RUN apk add --no-cache libc6-compat # 复制JAR包COPY app.jar /opt/ # 启动命令:限制堆内存为256MB(资源受限场景)CMD ["java", "-Xmx256m", "-jar", "/opt/app.jar"]一键获取完整项目代码模板3:Maven 项目多阶段构建# 阶段1:编译阶段(用JDK+Maven编译源码)FROM eclipse-temurin:21-jdk-ubuntu-jammy AS buildWORKDIR /src# 复制Maven配置与源码COPY pom.xml ./COPY src ./src# 安装Maven并编译RUN apt update && apt install -y maven && mvn clean package -DskipTests # 阶段2:运行阶段(仅JRE)FROM eclipse-temurin:21-jre-ubuntu-jammyWORKDIR /opt/app# 复制编译结果COPY --from=build /src/target/app.jar ./ # 启动命令CMD ["java", "-Xmx512m", "-jar", "app.jar"]一键获取完整项目代码5.3 常见问题排查表问题现象    可能原因    解决办法镜像拉取慢、频繁超时    未配置镜像访问支持或网络不稳定    1. 执行“一键安装脚本”配置轩辕加速;2. 检查网络是否通畅容器启动报错“Java version mismatch”    应用依赖的Java版本与镜像版本不一致    1. 查看应用文档确认所需Java版本;2. 更换对应版本的OpenJDK镜像应用启动报错“NoClassDefFoundError”    1. 依赖库缺失;2. Alpine镜像musl libc与JNI/native库不兼容    1. 确认JAR包依赖完整;2. 切换为Ubuntu镜像或安装libc6-compat容器内存溢出(OOM)    1. JVM最大堆内存(-Xmx)超过容器内存限制;2. 未限制容器资源    1. 减小-Xmx值(如从1g改为512m);2. 启动容器时添加--memory参数限制资源非 root 用户无法读取 JAR 包    复制JAR包时未设置正确权限    1. 复制时添加--chown=非root用户ID:组ID;2. 手动修改权限(RUN chmod 644 /opt/app/app.jar)多容器部署内存占用高    未启用类数据共享(CDS)或JVM参数未优化    1. 使用ibm-semeru-runtimes镜像并启用CDS(仅OpenJ9适用);2. 配置-Xmx与-Xms参数JVM 未适配容器资源    1. Java版本低于8u191/11;2. cgroup未正常生效    1. 升级OpenJDK镜像版本;2. 检查容器运行时环境的cgroup配置总结Docker 部署 OpenJDK 的全流程可概括为“环境搭建→镜像选择→构建部署→优化运维”四步:先通过一键脚本快速搭建 Docker 环境;再避开弃用的官方镜像,选择 eclipse-temurin 等替代方案,优先锁定 LTS 版本与具体镜像标签;接着根据应用场景选择基础构建或多阶段构建,同时配置非 root 用户与容器资源限制;最后通过 JVM 参数优化、类数据共享、定期安全扫描等手段,保障生产环境的稳定性与安全性。本文的实操步骤、模板与排查方案均经过企业级场景验证,可直接应用于 Java 微服务、Spring Boot 应用等容器化部署需求,同时兼顾了兼容性、安全性与资源效率。————————————————原文链接:https://blog.csdn.net/java_logo/article/details/156513813
  • [技术干货] 聊聊java的多线程
    1.什么是多线程定义:多线程是指在一个程序中同时执行多个线程的技术。每个线程代表一个独立的执行路径,但共享相同的内存空间和系统资源。优势:提高CPU利用率​ :当一个线程等待I/O操作时,另一个线程可以继续执行改善响应性​ :用户界面保持响应,后台处理任务并发处理任务​ : 同时处理多个请求或计算平时我们使用的大多都是单线程也就是main线程,虽然已经可以完成大部分的工作,但是如果任务量一旦多起来,那么你程序的吞吐量或许会指数型下降。这时候,能多点“帮手”一起完成任务就是至关重要的了,接下来我们来说下如何创建多线程2.多线程的常见实现方式2.1 继承Thread类这是最经典也是最简单的实现方式,只需要自定义类继承java.lang.Thread类,重写其run()方法,run()方法中定义了线程执行的具体任务。创建该类的实例后,通过调用start()方法启动线程。class MyThread extends Thread {    @Override    public void run() {        // 线程执行的代码    }} // 使用MyThread thread1 = new MyThread();thread1.start();  // 启动线程一键获取完整项目代码java使用Thread虽然简单,但是有个非常显而易见的缺点:由于java只支持单继承,所以MyThread这个类不能再继承其他的父类。2.2实现Runnable接口实现Runnable接口也可以创建多线程,并且没有继承Thread类的缺点,这也是开发中推荐使用的多线程实现的方式之一class MyRunnable implements Runnable {    @Override    public void run() {        // 线程执行的代码     }} // 使用Thread thread2 = new Thread(new MyRunnable());thread2.start();一键获取完整项目代码java2.3实现Callable接口与Futurejava.util.concurrent.Callable接口类似于Runnable,不同点在于Callable的call()方法可以有返回值并且可以抛出异常。要执行Callable任务,需将它包装进一个FutureTask,因为Thread类的构造器只接受Runnable参数,而FutureTask实现了Runnable接口。class MyCallable implements Callable<Integer> {    @Override    public Integer call() throws Exception {        // 线程执行的代码,这里返回一个整型结果        return 1;    }} public static void main(String[] args) {    MyCallable task = new MyCallable();     //使用FutureTask包装    FutureTask<Integer> futureTask = new FutureTask<>(task);    Thread t = new Thread(futureTask);        t.start();     try {        Integer result = futureTask.get(); // 获取线程执行结果         System.out.println("Result: " + result);    } catch (InterruptedException | ExecutionException e) {        e.printStackTrace();    }}一键获取完整项目代码java可以看到,使用这种方法编程稍微有些复杂,所以我更推荐平时使用第二种方式去开启线程3.线程的并发安全问题3.1问题抛出在jvm的内存结构中,多线程之间有一块共享的区域是堆内存,该区域经常存放对象、数组等,简单来说,平时new出来的对象大部分都放在了堆内存中,此时如果我们随意地使用多线程就会引发一个严重的问题——线程并发安全问题。举个简单的例子:有100张票,3个线程去分,结果会如何?//开启三个线程Thread thread1 = new Mythread();Thread thread2 = new Mythread();Thread thread3 = new Mythread(); thread1.start();thread2.start();thread3.start(); //主线程休眠1秒Thread.sleep(1000);System.out.println("执行了"+ticket.count+"次");一键获取完整项目代码java 我们可以清楚的观察到不仅票的数量不是递减的,总执行的次数也对不上,这就是多线程环境下的并发安全问题。3.2解析问题问题的根本就是ticket是全局共享的,对于对象成员变量的修改,线程会先拿到值后再做修改,由于这两步并不是同时进行的,所以会导致在一个线程做修改之前,另一个线程拿到了修改前的值,比如t1先取100,在做-1之前,t2也取到了100,两个线程先后做-1操作,此时t3来拿值,取到了98,不仅少了99这个状态,票数100也出现了两次,在打印的时候,由于各个线程顺序的不确定性,也会出现后打印的票数比前打印的票数多的情况。那怎么解决问题呢?3.3 synchronized关键字定义:Java语言的关键字,用于实现线程同步。当修饰方法或代码块时,同一时间仅允许一个线程执行该同步区域,其他线程需等待当前线程释放锁。synchronized就像是一把锁,当一个线程想要执行被synchronized修饰的方法时,就必须先拿到锁才能执行,之后又有线程想执行该方法后就会被阻塞,直到锁被释放才能去竞争锁,竞争成功后就可以执行方法synchronized有三种实现方法:1.同步实例方法public class BankAccount {    private int balance = 1000;        // 锁住当前账户对象(this)    public synchronized void withdraw(int amount) {        if (balance >= amount) {            balance -= amount;        }    }    }一键获取完整项目代码java由于锁的是同步方法,所以实际上是对这个对象(this)进行了上锁,这就导致只有在多线程一起使用这个对象的时候才可以实现数据隔离,如果又new了一个BankAccount对象,这时候就不能实现隔离,举个简单的例子,把这个对象当作是一间房子,锁同步方法仅仅只能防止别人进你家,不能防止别人进其他人家。2.同步静态方法public class BankAccount {    private int balance = 1000;        // 锁住当前账户对象(this)    public static synchronized void withdraw(int amount) {        if (balance >= amount) {            balance -= amount;        }    }    }一键获取完整项目代码javastatic修饰后的方法就是静态方法,生命周期上升到类的级别,与类强绑定,这时候使用synchronized修饰后相当于锁住了整个类,由于类是全局唯一的,所以就解决了创建多对象后无法实现数据隔离的情况了,再拿刚刚例子来说,这次别人既进不来你家,也进不去其他的房子里。万事大吉了!3.同步代码块public class BankAccount {    private int balance = 1000;     private final Object lock = new Object();  // 专门的锁对象        // 锁住当前账户对象(this)    public  void withdraw(int amount) {       synchronized(lock) { // 使用专门的锁对象        if (balance >= amount) {            balance -= amount;        }      }    }    }一键获取完整项目代码javapublic class BankAccount {    private int balance = 1000;        // 锁住当前账户对象(this)    public  void withdraw(int amount) {       synchronized(Object.class) { // 使用全局的类上锁        if (balance >= amount) {            balance -= amount;        }      }    }    }一键获取完整项目代码java相比于前两种,这一种方法可以做到锁的粒度更细,性能会有所提升,在synchronized()中,你既可以模拟第一种方法锁住对象,也可以模拟第二种方法使用类去全局上锁,两者效果均不变。3.4解决问题好了,我们已经大概了解了synchronized关键字的使用,接下来就是解决遗留的问题了,方法很简单,直接在buyTicket()前使用synchronized关键字修饰一下即可。  public synchronized static void buyTicket() {            ticketid--;            System.out.println(Thread.currentThread().getName() + "买了票,现在还剩下" + ticketid + "张");            count++;    }一键获取完整项目代码java加上sychronized后我们再来查看结果 现在无论我们执行多少次,结果都不会出问题了。————————————————原文链接:https://blog.csdn.net/gdpu2400502251/article/details/156653505
  • [技术干货] Maven仓库|Java/Gradle
    Maven 是一款软件的工程管理和自动构建工具,基于工程对象模型(POM)的概念,奉行约定优于配置原则,主要面向Java开发。Maven是一个基于插件的框架,通过插件执行java开发中各种自动化任务,可以灵活扩展和自定义。另一方面由于有统一的约定,形成标准,插件执行可共享也可重用,极大地提升效率。更多Maven相关内容,请访问 Maven 详细教程包依赖管理是maven的重要特性之一。随着开源的运动的发展,几乎所有的软件都不可避免的使用到第三方的开源库,java的开源类库非常丰富,我们可以通过依赖的方式方便地引入到工程中使用。但随着依赖增多版本不一致、版本冲突、依赖臃肿等问题都会接踵而来,maven通过坐标(GAV)标准化地定义了每一个开源组件和依赖关系,漂亮地解决了这些问题。同时Maven还提供了一个免费中央仓,让开发者可以方便地找到全球大部分需要的第三方库。Maven 仓库 用以存储和分发 Java/Gradle 项目所依赖的 jar 包。Maven中央仓库(https://repo1.maven.org/maven2)是 Maven 默认的仓库,存放了所有 Maven 项目所依赖的 jar 包,但是由于网络原因下载速度较慢。在国内有些镜像仓库,如阿里云、华为云、腾讯云等,可以加速 Maven 仓库的访问。本文默认配置基于阿里云 Maven仓库。仓库配置maven 配置指南打开 maven 的settings.xml配置文件 ,在 <mirrors></mirrors> 标签中添加 mirror 子节点:项目配置:maven 安装目录的 conf/settings.xml用户配置:或在用户家目录的 ~/.m2/ 文件夹下系统全局配置:maven安装目录下的conf目录中的setting.xml<mirror>  <id>aliyunmaven</id>  <mirrorOf>*</mirrorOf>  <name>阿里云公共仓库</name>  <url>https://maven.aliyun.com/repository/public</url></mirror>一键获取完整项目代码xml如果想使用其它代理仓库,可在<repositories></repositories>节点中加入对应的仓库使用地址。以使用 central 代理仓为例:<repository>  <id>central</id>  <url>https://maven.aliyun.com/repository/central</url>  <releases>    <enabled>true</enabled>  </releases>  <snapshots>    <enabled>true</enabled>  </snapshots></repository>一键获取完整项目代码xml在你的 pom.xml 文件<denpendencies></denpendencies>节点中加入你要引用的文件信息:<dependency>  <groupId>[GROUP_ID]</groupId>  <artifactId>[ARTIFACT_ID]</artifactId>  <version>[VERSION]</version></dependency>一键获取完整项目代码xml执行拉取命令:mvn install一键获取完整项目代码1gradle 配置指南在 build.gradle 文件中加入以下代码:allprojects {  repositories {    maven {      url 'https://maven.aliyun.com/repository/public/'    }    mavenLocal()    mavenCentral()  }}一键获取完整项目代码gradle如果想使用其它代理仓,以使用 central 仓为例,代码如下:allprojects {  repositories {    maven {      url 'https://maven.aliyun.com/repository/public/'    }    maven {      url 'https://maven.aliyun.com/repository/central'    }    mavenLocal()    mavenCentral()  }}一键获取完整项目代码gradle加入你要引用的文件信息:dependencies {  compile '[GROUP_ID]:[ARTIFACT_ID]:[VERSION]'}一键获取完整项目代码执行命令:gradle dependencies 或 ./gradlew dependencies 安装依赖一键获取完整项目代码仓库列表仓库名称    阿里云仓库地址    源地址central    https://maven.aliyun.com/repository/central    https://repo1.maven.org/maven2/public    https://maven.aliyun.com/repository/public    central仓和jcenter仓的聚合仓gradle-plugin    https://maven.aliyun.com/repository/gradle-plugin    https://plugins.gradle.org/m2/apache snapshots    https://maven.aliyun.com/repository/apache-snapshots    https://repository.apache.org/snapshots/配置其他镜像华为云华为云 提供 Maven Central,Grails,Jcenter 的 Java 开源组件。 登录后可获取 3~5MB/s CDN 下载加速地址,下载速度提升10倍。<mirror>    <id>huaweicloudmaven</id>    <name>华为云公共仓库</name>    <url>https://mirrors.huaweicloud.com/repository/maven/</url>    <mirrorOf>central</mirrorOf></mirror>一键获取完整项目代码打开maven的设置文件 settings.xml ,配置如下 repository mirror :————————————————原文链接:https://blog.csdn.net/mycosmos/article/details/156236710
  • [技术干货] 【Java 开发日记】我们来说一说 Redis IO 多路复用模型
    前言Redis 采用单线程 Reactor 模式处理客户端请求,其高性能的核心就在于 I/O 多路复用 技术。一、基础概念1. 什么是 I/O 多路复用?核心思想:使用一个进程/线程同时监听多个文件描述符(Socket),当某些描述符就绪(可读/可写)时,通知程序进行相应操作。解决的问题:避免为每个连接创建线程/进程带来的资源消耗,实现高并发连接处理。2. Redis 的架构选择# 传统多线程模型 vs Redis单线程+多路复用传统模型:1个连接 → 1个线程 → 高内存消耗、上下文切换开销大Redis模型:N个连接 → 1个线程 + I/O多路复用 → 低内存、无锁、高效一键获取完整项目代码二、Redis 中多路复用的实现1. 支持的底层机制Redis 在不同操作系统下使用不同的多路复用实现:Linux: epoll(最优选择)macOS/BSD: kqueueSolaris: evport其他 Unix: select(性能较差,备选)Redis 通过 ae(Async Event)抽象层统一封装这些接口。2. 核心工作流程1. 初始化服务器,监听端口2. 将监听套接字注册到多路复用器3. 进入事件循环:通过多路复用器等待事件(阻塞调用)事件就绪后返回:新连接到达 → 接受连接,注册读事件数据可读 → 读取命令,解析,放入命令队列可写事件 → 将响应数据发送给客户端c) 处理时间事件(定时任务)4. 循环执行步骤 3三、源码级实现解析1. 事件循环结构typedef struct aeEventLoop {    int maxfd;                   // 当前最大文件描述符    int setsize;                 // 监听的文件描述符数量上限    long long timeEventNextId;   // 下一个时间事件ID    aeFileEvent *events;         // 文件事件数组    aeFiredEvent *fired;         // 就绪事件数组    aeTimeEvent *timeEventHead;  // 时间事件链表头    void *apidata;               // 多路复用器的特定数据(epoll/kqueue等)    aeBeforeSleepProc *beforesleep;    aeBeforeSleepProc *aftersleep;} aeEventLoop;一键获取完整项目代码2. 事件注册过程// 以 epoll 为例的简化逻辑int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData) {    // 1. 在 events 数组中记录事件处理器    aeFileEvent *fe = &eventLoop->events[fd];     // 2. 调用底层 API 注册事件    if (aeApiAddEvent(eventLoop, fd, mask) == -1)        return -1;     // 3. 设置回调函数    fe->mask |= mask;    if (mask & AE_READABLE) fe->rfileProc = proc;    if (mask & AE_WRITABLE) fe->wfileProc = proc;    fe->clientData = clientData;     return 0;}一键获取完整项目代码3. 事件分发循环void aeMain(aeEventLoop *eventLoop) {    eventLoop->stop = 0;    while (!eventLoop->stop) {        // 处理事件前执行的操作(如处理异步任务)        if (eventLoop->beforesleep != NULL)            eventLoop->beforesleep(eventLoop);         // 核心:多路复用等待事件        aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_AFTER_SLEEP);    }} int aeProcessEvents(aeEventLoop *eventLoop, int flags) {    // 1. 计算最近的时间事件,确定多路复用的超时时间    // 2. 调用多路复用API(epoll_wait/kevent/select等)    numevents = aeApiPoll(eventLoop, tvp);     // 3. 遍历就绪事件,调用相应的回调函数    for (j = 0; j < numevents; j++) {        aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];         if (fe->mask & mask & AE_READABLE) {            fe->rfileProc(eventLoop, fd, fe->clientData, mask);        }        if (fe->mask & mask & AE_WRITABLE) {            fe->wfileProc(eventLoop, fd, fe->clientData, mask);        }    }     // 4. 处理时间事件    if (flags & AE_TIME_EVENTS)        processed += processTimeEvents(eventLoop);     return processed;}一键获取完整项目代码四、性能优化细节1. 为什么 Redis 能单线程处理高并发?纯内存操作:数据操作在内存中完成,速度极快非阻塞I/O:所有Socket设置为非阻塞模式批量命令处理:支持管道(pipeline),减少网络往返高效数据结构:精心优化的数据结构实现2. epoll 的优势(Linux环境下)# select/poll 的局限性1. 每次调用都需要传递所有监听的fd(用户空间→内核空间复制)2. 内核需要遍历所有fd检查就绪状态 O(n)3. 支持的文件描述符数量有限(select默认1024) # epoll 的优化1. epoll_create: 创建epoll实例2. epoll_ctl: 添加/修改/删除fd(仅增量更新)3. epoll_wait: 获取就绪事件(仅返回就绪的fd)4. 使用红黑树管理fd,哈希表存储就绪列表 O(1)复杂度一键获取完整项目代码五、多线程扩展(Redis 6.0+)Redis 6.0 引入了多线程I/O,但注意: 配置示例(redis.conf):# 开启多线程I/Oio-threads 4          # 启用4个I/O线程(通常设为CPU核心数)io-threads-do-reads yes  # 启用读多线程(写默认开启)一键获取完整项目代码六、与其他模型的对比模型连接管理并发能力复杂度适用场景阻塞I/O+多线程每连接一线程受限于线程数高传统数据库多进程每连接一进程受限于进程数高Apache prefork异步I/O完全异步非常高很高Nginx, Node.jsRedis模型多路复用+单线程高(10万+QPS)中内存数据库、缓存七、实际监控与调优1. 监控指标# 查看Redis事件循环状态redis-cli info stats | grep -E "(total_connections_received|instantaneous_ops_per_sec|total_commands_processed)" # 查看网络I/Oredis-cli info stats | grep -E "(total_net_input_bytes|total_net_output_bytes|rejected_connections)"一键获取完整项目代码2. 性能瓶颈识别CPU瓶颈:单核跑满,考虑分片或升级CPU网络瓶颈:网络吞吐达到上限内存瓶颈:OOM或频繁交换阻塞操作:慢查询、大key、持久化阻塞3. 配置建议# 调整最大连接数(根据实际情况)maxclients 10000 # 调整TCP backlogtcp-backlog 511 # 调整客户端超时timeout 0  # 永不断开,适合内网 # 合理设置内存淘汰策略maxmemory-policy allkeys-lru一键获取完整项目代码八、总结Redis 的 I/O 多路复用模型是其高性能的基石:单线程事件循环避免了锁竞争和上下文切换多路复用技术高效管理大量连接纯内存操作保证极快的响应速度渐进式演进在保持核心简单的同时引入多线程优化I/O面试回答Redis 之所以这么快,IO 多路复用模型是很关键的一点。我通俗地解释一下它的工作原理:假设 Redis 是一个餐厅服务员,传统的阻塞 IO 就像是一个服务员每次只服务一桌客人,点菜、上菜都要等这一桌完事了才能服务下一桌,这样效率很低。而 IO 多路复用呢,就像是这个服务员同时监听多个桌子的呼叫铃。服务员站在大厅里,哪一桌有需求(比如客户端发来了读写请求),他就过去处理一下,处理完马上回来继续监听。这样一个人就能同时照顾很多桌客人,效率大大提升。在技术实现上,Redis 底层使用的是像 select、poll这样的系统调用。它们的作用就是帮 Redis 监听大量的网络连接,一旦某个连接有数据可读或可写,就通知 Redis 去处理,而不用为每个连接创建一个线程去阻塞等待。这样做的好处很明显:高性能:单线程就能处理大量并发连接,避免了多线程的上下文切换开销。低延迟:因为事件是即时有响应就处理,不会长时间阻塞。资源省:不需要为每个连接创建线程,内存和 CPU 消耗都更小。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/155535520
  • [技术干货] JavaScript 核心特性综合实战 —— 从函数到对象的深度应用
    函数语法格式// 创建函数/函数声明/函数定义function 函数名(形参列表) {    函数体    return 返回值;} // 函数调用函数名(实参列表)        // 不考虑返回值返回值 = 函数名(实参列表) // 考虑返回值一键获取完整项目代码javascript函数定义并不会执行函数体内容,必须要调用才会执行,调用几次就会执行几次。function hello() {    console.log("hello");}// 如果不调用函数,则没有执行打印语句hello();一键获取完整项目代码javascript调用函数的时候进入函数内部执行,函数结束时回到调用位置继续执行,可以借助调试器来观察。函数的定义和调用的先后顺序没有要求(这一点和变量不同,变量必须先定义再使用)// 调用函数hello();// 定义函数function hello() {    console.log("hello");}一键获取完整项目代码javascript关于参数个数实参和形参之间的个数可以不匹配,但是实际开发一般要求形参和实参个数要匹配如果实参个数比形参个数多,则多出的参数不参与函数运算sum(10, 20, 30); // 30一键获取完整项目代码javascript如果实参个数比形参个数少,则此时多出来的形参值为 undefinedsum(10); // NaN,相当于 num2 为 undefined一键获取完整项目代码javascriptJS 的函数传参比较灵活,这一点和其他语言差别较大,事实上这种灵活性往往不是好事。函数表达式另外一种函数的定义方式var add = function() {    var sum = 0;    for (var i = 0; i < arguments.length; i++) {        sum += arguments[i];    }    return sum;}console.log(add(10, 20));         // 30console.log(add(1, 2, 3, 4));     // 10console.log(typeof add);          // function一键获取完整项目代码javascript此时形如 function() {} 这样的写法定义了一个匿名函数,然后将这个匿名函数用一个变量来表示,后面就可以通过这个 add 变量来调用函数了。JS 中函数是一等公民,可以用变量保存,也可以作为其他函数的参数或者返回值。作用域某个标识符名字在代码中的有效范围。在 ES6 标准之前,作用域主要分成两个:全局作用域:在整个 script 标签中,或者单独的 js 文件中生效。局部作用域 / 函数作用域:在函数内部生效。// 全局变量var num = 10;console.log(num); function test() {    // 局部变量    var num = 20;    console.log(num);} function test2() {    // 局部变量    var num = 30;    console.log(num);} test();test2();console.log(num); // 执行结果10203010一键获取完整项目代码javascript创建变量时如果不写 var,则得到一个全局变量。function test() {    num = 100;}test();console.log(num); // 执行结果100一键获取完整项目代码javascript另外,很多语言的局部变量作用域是按照代码块(大括号)来划分的,JS 在 ES6 之前不是这样的。if (1 < 2) {    var a = 10;}console.log(a);一键获取完整项目代码javascript作用域链背景:函数可以定义在函数内部内层函数可以访问外层函数的局部变量内部函数可以访问外部函数的变量,采取的是链式查找的方式,从内到外依次进行查找。var num = 1;function test1() {    var num = 10;     function test2() {        var num = 20;        console.log(num);    }     test2();}test1(); // 执行结果20一键获取完整项目代码javascript执行 console.log(num) 的时候,会现在 test2 的局部作用域中查找 num,如果没找到,则继续去 test1 中查找,如果还没找到,就去全局作用域查找。 对象基本概念对象是指一个具体的事物。“电脑” 不是对象,而是一个泛指的类别,而 “我的联想笔记本” 就是一个对象。在 JS 中,字符串、数值、数组、函数都是对象。每个对象中包含若干的属性和方法:属性:事物的特征。方法:事物的行为。例如,你有一个女票:她的身高、体重、三围这些都是属性。她的唱歌、跳舞、暖床都是方法。对象需要保存的属性有多个,虽然数组也能用于保存多个数据,但是不够好。例如表示一个学生信息(姓名蔡徐坤,身高 175cm,体重 170 斤):var student = ["蔡徐坤", 175, 170];一键获取完整项目代码javascript但是这种情况下到底 175 和 170 谁表示身高,谁表示体重,就容易分不清。JavaScript 的对象和 Java 的对象概念上基本一致,只是具体的语法表现形式差别较大。1. 使用字面量创建对象 [常用]使用 {} 创建对象var a = {}; // 创建了一个空的对象 var student = {    name: '蔡徐坤',    height: 175,    weight: 170,    sayHello: function() {        console.log("hello");    }};一键获取完整项目代码javascript使用 {} 创建对象属性和方法使用键值对的形式来组织。键值对之间使用 , 分割,最后一个属性后面的 , 可有可无。键和值之间使用 : 分割。方法的值是一个匿名函数。使用对象的属性和方法:// 1. 使用 . 成员访问运算符来访问属性,. 可以理解成“的”console.log(student.name);// 2. 使用 [] 访问属性,此时属性需要加上引号console.log(student["height"]);// 3. 调用方法,别忘记加上 ()student.sayHello();一键获取完整项目代码javascript2. 使用 new Object 创建对象var student = new Object(); // 和创建数组类似student.name = "蔡徐坤";student.height = 175;student["weight"] = 170;student.sayHello = function () {    console.log("hello");} console.log(student.name);console.log(student["weight"]);student.sayHello();一键获取完整项目代码javascript注意:使用 {} 创建的对象也可以随时使用 student.name = "蔡徐坤"; 这样的方式来新增属性。3. 使用构造函数创建对象前面的创建对象方式只能创建一个对象,而使用构造函数可以很方便的创建多个对象。例如:创建几个猫咪对象var mimi = {    name: "咪咪",    type: "中华田园喵",    miao: function () {        console.log("喵");    }}; var xiaohei = {    name: "小黑",    type: "波斯喵",    miao: function () {        console.log("猫呜");    }}; var ciqiu = {    name: "刺球",    type: "金渐层",    miao: function () {        console.log("咕噜噜");    }}一键获取完整项目代码javascript此时写起来就比较麻烦,使用构造函数可以把相同的属性和方法的创建提取出来,简化开发过程。基本语法function 构造函数名(形参) {    this.属性 = 值;    this.方法 = function...} var obj = new 构造函数名(实参);一键获取完整项目代码javascript注意:在构造函数内部使用 this 关键字来表示当前正在构建的对象。构造函数的函数名首字母一般是大写的。构造函数的函数名可以是名词。构造函数不需要 return。创建对象的时候必须使用 new 关键字。this 相当于 “我”使用构造函数重新创建猫咪对象function Cat(name, type, sound) {    this.name = name;    this.type = type;    this.miao = function () {        console.log(sound); // 别忘了作用域的链式访问规则    }} var mimi = new Cat("咪咪", "中华田园喵", "喵");var xiaohei = new Cat("小黑", "波斯喵", "猫呜");var ciqiu = new Cat("刺球", "金渐层", "咕噜噜"); console.log(mimi);mimi.miao();一键获取完整项目代码javascript理解 new 关键字new 的执行过程:先在内存中创建一个空的对象 {}this 指向刚才的空对象(将上一步的对象作为 this 的上下文)执行构造函数的代码,给对象创建属性和方法返回这个对象(构造函数本身不需要 return,由 new 代劳了)参考 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/newJavaScript 的对象和 Java 的对象的区别1.JavaScript 没有 “类” 的概念对象其实就是 “属性 + 方法”。类相当于把一些具有共性的对象的属性和方法单独提取了出来,相当于一个 “月饼模子”。在 JavaScript 中的 “构造函数” 也能起到类似的效果。而且即使不是用构造函数,也可以随时的通过 {} 的方式指定出一些对象。在 ES6 中也引入了 class 关键字,就能按照类似于 Java 的方式创建类和对象了。2.JavaScript 对象不区分 “属性” 和 “方法”JavaScript 中的函数是 “一等公民”,和普通的变量一样,存储了函数的变量能够通过 () 来进行调用执行。3.JavaScript 对象没有 private /public 等访问控制机制对象中的属性都可以被外界随意访问。4.JavaScript 对象没有 “继承”继承本质就是 “让两个对象建立关联”,或者说是让一个对象能够重用另一个对象的属性 / 方法。JavaScript 中使用 “原型” 机制实现类似的效果。例如:创建一个 cat 对象和 dog 对象,让这两个对象都能使用 animal 对象中的 eat 方法。通过 __proto__ 属性来建立这种关联关系(proto 翻译作 “原型”) 5.JavaScript 没有 “多态”多态的本质在于 “程序不必关注具体的类型,就能使用其中的某个方法”。C++/Java 等静态类型的语言对于类型的约束和校验比较严格,因此通过子类继承父类,并重写父类的方法的方式来实现多态的效果。但是在 JavaScript 中本身就支持动态类型,程序猿在使用对象的某个方法的时候本身也不需要对对象的类型做出明确区分,因此并不需要在语法层面上支持多态。例如:在 Java 中已经学过 ArrayList 和 LinkedList,为了让程序猿使用方便,往往写作:List<String> list = new ArrayList<>();一键获取完整项目代码然后我们可以写一个方法:void add(List<String> list, String s) {    list.add(s);}一键获取完整项目代码我们不必关注 list 是 ArrayList 还是 LinkedList,只要是 List 就行,因为 List 内部带有 add 方法。当我们使用 JavaScript 的代码的时候:function add(list, s) {    list.add(s)}一键获取完整项目代码javascriptadd 对于 list 这个参数的类型本身就没有任何限制,只需要 list 这个对象有 add 方法即可,就不必像 Java 那样先继承再重写绕一个圈子。————————————————原文链接:https://blog.csdn.net/HANhylyxy/article/details/156615201
  • [技术干货] 【Java 开发日记】有了解过 SpringBoot 的参数配置吗?
    当然了解,Spring Boot 的参数配置是其核心特性之一,也是它实现“约定大于配置”理念的关键。它极大地简化了传统 Spring 应用中繁琐的 XML 配置。一、核心概念:application.properties 与 application.ymlSpring Boot 默认使用这两种文件进行配置(二者选其一即可,.yml 更常用)。application.properties (传统键值对格式)server.port=8081spring.datasource.url=jdbc:mysql://localhost:3306/mydbspring.datasource.username=rootspring.datasource.password=secretlogging.level.com.example.demo=debug运行项目并下载源码application.yml (YAML 格式,层次感更强,推荐使用)server:  port: 8081 spring:  datasource:    url: jdbc:mysql://localhost:3306/mydb    username: root    password: secret logging:  level:    com.example.demo: debug运行项目并下载源码YAML 注意事项:缩进必须使用空格,不能使用 Tab 键,冒号后面必须有一个空格。二、配置的加载位置与优先级Spring Boot 会从以下位置按从高到低的优先级加载 application 配置文件(高优先级的配置会覆盖低优先级的配置):当前项目根目录下的 /config 子目录当前项目根目录classpath 下的 /config 包 (即 src/main/resources/config)classpath 根路径 (即 src/main/resources)最佳实践:在开发时,将通用配置放在 src/main/resources/application.yml 中。在打包部署时,可以在 JAR 包所在目录创建一个 config 文件夹,里面放一个 application.yml 来覆盖开发环境的配置(如数据库连接),这样就实现了配置与代码分离。三、外部化配置(非常强大)除了配置文件,Spring Boot 还支持多种外部配置方式,优先级高于 application.yml。这在容器化部署(如 Docker)时尤其有用。命令行参数java -jar yourapp.jar --server.port=8888 --spring.datasource.url=jdbc:mysql://prod-server:3306/proddb运行项目并下载源码操作系统环境变量Spring Boot 会自动识别形如 SPRING_DATASOURCE_URL 的环境变量(注意大小写和下划线)。Profile-specific 配置(多环境配置)这是管理不同环境(开发、测试、生产)配置的最佳方式。在通用的 application.yml 中,通过 spring.profiles.active 属性来激活特定环境的配置。配置文件命名规则:application-{profile}.yml例如:application-dev.yml (开发环境)application-test.yml (测试环境)application-prod.yml (生产环境)application.ymlspring:  profiles:    active: dev # 默认激活开发环境运行项目并下载源码激活方式:在配置文件中设置(如上所示)。命令行激活:java -jar yourapp.jar --spring.profiles.active=prodJVM 参数:-Dspring.profiles.active=test环境变量:export SPRING_PROFILES_ACTIVE=prod四、如何在代码中获取配置值?@Value 注解 (适用于单个属性)@Componentpublic class MyComponent {     @Value("${server.port}")    private int serverPort;     @Value("${app.message: Hello Default}") // 使用冒号指定默认值    private String message;     // ... }运行项目并下载源码@ConfigurationProperties 注解 (推荐,用于绑定一组配置)这是更类型安全、更面向对象的方式。步骤 1:在 application.yml 中定义配置app:  user:    name: "Alice"    age: 30    email: "alice@example.com"    hobbies:      - reading      - hiking运行项目并下载源码步骤 2:创建一个配置类来绑定这些属性@Component@ConfigurationProperties(prefix = "app.user") // 前缀是 app.user@Data // Lombok 注解,自动生成 getter/setter// 或者也可以手动写 getter 和 setterpublic class UserProperties {    private String name;    private Integer age;    private String email;    private List<String> hobbies;}运行项目并下载源码步骤 3:在需要的地方注入并使用@Servicepublic class MyService {     @Autowired    private UserProperties userProperties;     public void doSomething() {        System.out.println("User name: " + userProperties.getName());        System.out.println("User hobbies: " + userProperties.getHobbies());    }}运行项目并下载源码别忘了在启动类上添加 @EnableConfigurationProperties 注解(但如果你像上面一样在配置类上使用了 @Component,则不需要)。五、常用配置示例# 服务器配置server:  port: 8080  servlet:    context-path: /api # 应用上下文路径 # 数据源配置spring:  datasource:    url: jdbc:mysql://localhost:3306/test    username: root    password: 123456    driver-class-name: com.mysql.cj.jdbc.Driver  # JPA 配置  jpa:    hibernate:      ddl-auto: update # 生产环境不要用 create-drop 或 update    show-sql: true # 日志配置logging:  level:    root: info    org.springframework.web: debug    com.example: trace  file:    name: logs/myapp.log # 输出到文件 # 自定义配置myapp:  feature:    enabled: true    api-url: https://api.example.com运行项目并下载源码总结Spring Boot 的参数配置系统非常灵活和强大,其核心思想是:约定大于配置:提供了大量默认配置,开箱即用。配置外部化:允许你通过文件、命令行、环境变量等多种方式覆盖默认配置,轻松适应不同环境。类型安全绑定:通过 @ConfigurationProperties 可以轻松地将一组配置映射到 Java Bean 上,是管理自定义配置的首选方式————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/154401707
  • [技术干货] 【Java 开发日记】我们来说说 ThreadLocal 的原理,使用场景及内存泄漏问题
    一、核心原理1. 数据存储结构// 每个 Thread 对象内部都有一个 ThreadLocalMapThreadLocal.ThreadLocalMap threadLocals = null; // ThreadLocalMap 内部使用 Entry 数组,Entry 继承自 WeakReference<ThreadLocal<?>>static class Entry extends WeakReference<ThreadLocal<?>> {    Object value;    Entry(ThreadLocal<?> k, Object v) {        super(k);  // 弱引用指向 ThreadLocal 实例        value = v; // 强引用指向实际存储的值    }}一键获取完整项目代码2. 关键设计线程隔离:每个线程有自己的 ThreadLocalMap 副本哈希表结构:使用开放地址法解决哈希冲突弱引用键:Entry 的 key(ThreadLocal 实例)是弱引用延迟清理:set / get 时自动清理过期条目二、源码分析1. set() 方法流程public void set(T value) {    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null) {        map.set(this, value);  // this指当前ThreadLocal实例    } else {        createMap(t, value);    }} private void set(ThreadLocal<?> key, Object value) {    Entry[] tab = table;    int len = tab.length;    int i = key.threadLocalHashCode & (len-1);     // 遍历查找合适的位置    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {        ThreadLocal<?> k = e.get();         // 找到相同的key,直接替换value        if (k == key) {            e.value = value;            return;        }         // key已被回收,替换过期条目        if (k == null) {            replaceStaleEntry(key, value, i);            return;        }    }     tab[i] = new Entry(key, value);    int sz = ++size;    // 清理并判断是否需要扩容    if (!cleanSomeSlots(i, sz) && sz >= threshold)        rehash();}一键获取完整项目代码2. get() 方法流程public T get() {    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null) {        ThreadLocalMap.Entry e = map.getEntry(this);        if (e != null) {            @SuppressWarnings("unchecked")            T result = (T)e.value;            return result;        }    }    return setInitialValue();  // 返回初始值}一键获取完整项目代码三、使用场景1. 典型应用场景// 场景1:线程上下文信息传递(如Spring的RequestContextHolder)public class RequestContextHolder {    private static final ThreadLocal<HttpServletRequest> requestHolder =     new ThreadLocal<>();     public static void setRequest(HttpServletRequest request) {        requestHolder.set(request);    }     public static HttpServletRequest getRequest() {        return requestHolder.get();    }} // 场景2:数据库连接管理public class ConnectionManager {    private static ThreadLocal<Connection> connectionHolder =     ThreadLocal.withInitial(() -> DriverManager.getConnection(url));     public static Connection getConnection() {        return connectionHolder.get();    }} // 场景3:用户会话信息public class UserContext {    private static ThreadLocal<UserInfo> userHolder = new ThreadLocal<>();     public static void setUser(UserInfo user) {        userHolder.set(user);    }     public static UserInfo getUser() {        return userHolder.get();    }} // 场景4:避免参数传递public class TransactionContext {    private static ThreadLocal<Transaction> transactionHolder = new ThreadLocal<>();     public static void beginTransaction() {        transactionHolder.set(new Transaction());    }     public static Transaction getTransaction() {        return transactionHolder.get();    }}一键获取完整项目代码2. 使用建议声明为 private static final考虑使用 ThreadLocal.withInitial() 提供初始值在 finally 块中清理资源四、内存泄漏问题1. 泄漏原理强引用链:Thread → ThreadLocalMap → Entry[] → Entry → value (强引用)                                                    弱引用:                                                   Entry → key (弱引用指向ThreadLocal) 泄漏场景:1. ThreadLocal实例被回收 → key=null2. 但value仍然被Entry强引用3. 线程池中线程长期存活 → value无法被回收4. 导致内存泄漏一键获取完整项目代码2. 解决方案对比// 方案1:手动remove(推荐)try {    threadLocal.set(value);    // ... 业务逻辑} finally {    threadLocal.remove();  // 必须执行!} // 方案2:使用InheritableThreadLocal(父子线程传递)ThreadLocal<String> parent = new InheritableThreadLocal<>();parent.set("parent value"); new Thread(() -> {    // 子线程可以获取父线程的值    System.out.println(parent.get());  // "parent value"}).start(); // 方案3:使用FastThreadLocal(Netty优化版)// 适用于高并发场景,避免了哈希冲突一键获取完整项目代码3. 最佳实践public class SafeThreadLocalExample {    // 1. 使用static final修饰    private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));     // 2. 包装为工具类    public static Date parse(String dateStr) throws ParseException {        SimpleDateFormat sdf = DATE_FORMAT.get();        try {            return sdf.parse(dateStr);        } finally {            // 注意:这里通常不需要remove,因为要重用SimpleDateFormat            // 但如果是用完即弃的场景,应该remove        }    }     // 3. 线程池场景必须清理    public void executeInThreadPool() {        ExecutorService executor = Executors.newFixedThreadPool(5);         for (int i = 0; i < 10; i++) {            executor.submit(() -> {                try {                    UserContext.setUser(new UserInfo());                    // ... 业务处理                } finally {                    UserContext.remove();  // 关键!                }            });        }    }}一键获取完整项目代码五、注意事项线程池风险:线程复用导致数据污染继承问题:子线程默认无法访问父线程的ThreadLocal性能影响:哈希冲突时使用线性探测,可能影响性能空值处理:get()返回null时要考虑初始化六、替代方案方案适用场景优点缺点ThreadLocal线程隔离数据简单高效内存泄漏风险InheritableThreadLocal父子线程传递继承上下文线程池中失效TransmittableThreadLocal线程池传递线程池友好引入依赖参数传递简单场景无副作用代码冗余七、调试技巧// 查看ThreadLocalMap内容(调试用)public static void dumpThreadLocalMap(Thread thread) throws Exception {    Field field = Thread.class.getDeclaredField("threadLocals");    field.setAccessible(true);    Object map = field.get(thread);     if (map != null) {        Field tableField = map.getClass().getDeclaredField("table");        tableField.setAccessible(true);        Object[] table = (Object[]) tableField.get(map);         for (Object entry : table) {            if (entry != null) {                Field valueField = entry.getClass().getDeclaredField("value");                valueField.setAccessible(true);                System.out.println("Key: " + ((WeakReference<?>) entry).get()                                    + ", Value: " + valueField.get(entry));            }        }    }}一键获取完整项目代码ThreadLocal 是强大的线程隔离工具,但需要谨慎使用。在 Web 应用和线程池场景中,必须在 finally 块中调用 remove(),这是避免内存泄漏的关键。面试回答关于 ThreadLocal,我从原理、场景和内存泄漏三个方面来说一下我的理解。1. 首先,它的核心原理是什么?简单来说,ThreadLocal 是一个线程级别的变量隔离工具。它的设计目标就是让同一个变量,在不同的线程里有自己独立的副本,互不干扰。底层结构:每个线程(Thread对象)内部都有一个自己的 ThreadLocalMap(你可以把它想象成一个线程私有的、简易版的HashMap)。怎么存:当我们调用 ThreadLocal.set(value) 时,实际上是以当前的 ThreadLocal 实例自身作为 Key,要保存的值作为 Value,存入当前线程的那个 ThreadLocalMap 里。怎么取:调用 ThreadLocal.get() 时,也是用自己作为 Key,去当前线程的 Map 里查找对应的 Value。打个比方:就像去银行租保险箱。Thread 是银行,ThreadLocalMap 是银行里的一排保险箱,ThreadLocal 实例就是你手里那把特定的钥匙。你用这把钥匙(ThreadLocal实例)只能打开属于你的那个格子(当前线程的Map),存取自己的东西(Value),完全看不到别人格子的东西。不同的人(线程)即使用同一款钥匙(同一个ThreadLocal实例),打开的也是不同银行的格子,东西自然隔离了。2. 其次,它的典型使用场景有哪些?正是因为这种线程隔离的特性,它特别适合用来传递一些需要在线程整个生命周期内、多个方法间共享,但又不能(或不想)通过方法参数显式传递的数据。最常见的有两个场景:场景一:保存上下文信息(最经典)比如在 Web 应用 或 RPC 框架 中处理一个用户请求时,这个请求从进入系统到返回响应,全程可能由同一个线程处理。我们会把一些信息(比如用户ID、交易ID、语言环境)存到一个 ThreadLocal 里。这样,后续的任何业务方法、工具类,只要在同一个线程里,就能直接 get() 到这些信息,避免了在每一个方法签名上都加上这些参数,代码会简洁很多。场景二:管理线程安全的独享资源典型例子是 数据库连接 和 SimpleDateFormat。像 SimpleDateFormat 这个类,它不是线程安全的。如果做成全局共享,就要加锁,性能差。用 ThreadLocal 的话,每个线程都拥有自己的一个 SimpleDateFormat 实例,既避免了线程安全问题,又因为线程复用了这个实例,减少了创建对象的开销。类似的,在一些需要保证数据库连接线程隔离(比如事务管理)的场景,也会用到 ThreadLocal 来存放当前线程的连接。3. 最后,关于它的内存泄漏问题ThreadLocal 如果使用不当,确实可能导致内存泄漏。它的根源在于 ThreadLocalMap 中 Entry 的设计。问题根源:ThreadLocalMap 的 Key(也就是 ThreadLocal 实例)是一个 弱引用。这意味着,如果外界没有强引用指向这个 ThreadLocal 对象(比如我们把 ThreadLocal 变量设为了 null),下次垃圾回收时,这个 Key 就会被回收掉,于是 Map 里就出现了一个 Key 为 null,但 Value 依然存在的 Entry。这个 Value 是一个强引用,只要线程还活着(比如用的是线程池,线程会复用,一直不结束),这个 Value 对象就永远无法被回收,造成了内存泄漏。如何避免:良好习惯:每次使用完 ThreadLocal 后,一定要手动调用 remove() 方法。这不仅是清理当前值,更重要的是它会清理掉整个 Entry,这是最有效、最安全的做法。设计保障:ThreadLocal 本身也做了一些努力,比如在 set()、get()、remove() 的时候,会尝试去清理那些 Key 为 null 的过期 Entry。但这是一种“被动清理”,不能完全依赖。代码层面:尽量将 ThreadLocal 变量声明为 static final,这样它的生命周期就和类一样长,不会被轻易回收,减少了产生 null Key 的机会。但这并不能替代 remove(),因为线程池复用时,上一个任务的值可能会污染下一个任务。总结一下:内存泄漏的关键是 “弱Key + 强Value + 长生命周期线程” 的组合。所以,把 remove() 放在 finally 块里调用,是一个必须养成的编程习惯。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/156130616
  • [技术干货] Java智慧驾校系统源码:支持小程序/公众号,助力驾校数字化升级
    智慧驾培云平台:基于Java+SpringBoot的全渠道数字化驾考解决方案在驾培行业数字化转型的浪潮下,为应对传统管理模式中信息不透明、预约效率低、学习体验割裂等痛点,我们基于Java + SpringBoot + MyBatis-Plus + MySQL 这一稳健高效的技术栈,构建了覆盖PC管理后台、H5自适应网站、微信小程序及公众号的全方位智慧驾考平台,旨在为驾校赋能、为教练减负、为学员提供一站式、便捷高效的学车新体验。  平台核心价值与功能体系本平台致力于打通线上报名、智能预约、科学学习、进度跟踪与精细运营的全业务流程,构建一个连接学员、教练与驾校的数字化生态。多终端协同:无缝衔接的学车入口微信小程序/H5移动端(学员核心入口)驾校与教练发现:支持按地理位置、评分、价格筛选驾校与教练,并提供详细的教练档案(评分、教龄、已带学员数)。一站式在线报名:查看透明化的班型套餐与价目表,完成在线选班、信息填写与支付,流程极简。智能预约与排课:可视化日历展示教练可约时间,学员可自主选择时段一键预约,系统自动防冲突。即时消息触达:训练安排变更、考试通知、政策新规等重要信息通过公众号模板消息或小程序订阅消息实时推送。PC端运营管理后台(驾校管理核心)资源与教务管理:集中管理教练信息、车辆信息、训练场地与课程班型。智能排班与调度:可视化排课表,支持批量排课与智能调度,最大化利用教练与车辆资源。学员全生命周期管理:从报名、分科、训练到考试结业的全程档案跟踪与进度可视化。数据化运营看板:关键业务数据统计(报名转化率、教练课时量、学员通过率、财务流水),支撑科学决策。 全周期教学辅导:科学高效的备考体系四阶段科一至科四全覆盖科一/科四(理论):集成官方同步题库,提供章节练习、顺序练习、模拟考试等多种模式。科二/科三(实操):提供项目要点图文/视频详解、考试路线模拟、常见失误点分析等学习资源。智能化学习工具包个性化题库训练:支持收藏难题、自动生成错题本,助力针对性复习。高仿真模拟考试:完全模拟真实考试界面、流程与计时,帮助学员适应考场节奏。学习数据分析:实时统计各章节正确率、模拟考成绩趋势,生成个人能力雷达图与学习建议。进阶教学管理功能智能预约调度引擎:后端算法基于教练忙闲、学员进度、场地资源进行优化排期,减少空置与冲突。学员进度跟踪系统:自动记录练车课时、模拟成绩、教练评语,形成数字化学车档案。 技术架构深度解析 高性能、可扩展的后端服务架构核心框架:Spring Boot快速开发与微服务就绪:约定优于配置,内嵌Web服务器,轻松构建独立、生产级的应用,为未来服务拆分奠定基础。强大的并发处理:结合连接池优化与异步处理机制,从容应对报名、预约等业务高峰期的并发请求。统一的系统治理:集成全局异常处理、日志管理、参数校验与安全防护机制,保障系统稳定与安全。数据持久层:MyBatis-Plus极致开发效率:通过丰富的Lambda表达式与条件构造器,无需编写XML即可完成复杂查询,并内置通用CRUD方法。代码生成与维护性:支持基于数据库表反向生成实体、Mapper、Service代码,极大提升初期开发与后续维护效率。数据存储层:MySQL规范化的数据库设计:围绕核心业务实体进行设计,确保数据一致性与完整性。sql-- 核心业务表示例`coach`(教练表): id, name, avatar, teaching_years, rating, specialty, status`student`(学员表): id, user_id, enrolled_school, current_subject, overall_progress`training_course`(课程/班型表): id, name, price, description, include_subjects`appointment_record`(预约记录表): id, student_id, coach_id, vehicle_id, time_slot, status`question_bank`(题库表): id, subject, chapter, question_text, options, answer, analysis性能优化策略:针对查询频繁的表(如教练、课程)建立有效索引,对增长快速的业务数据(如预约、日志)制定归档策略。灵活统一的多端前端适配方案响应式Web应用(H5 + PC)采用前后端分离架构,后端提供统一API,前端使用现代框架(如Vue.js/React)构建。通过响应式CSS框架(如Element-Plus/Ant Design)实现一套代码自适应PC大屏与手机H5浏览器。微信生态集成(小程序 + 公众号)微信小程序:提供媲美原生应用的流畅体验,利用微信授权快速登录,集成地图选点、消息订阅等原生能力。微信公众号:作为重要信息下发渠道和服务入口,与小程序账号体系打通,实现菜单引导与轻量服务。统一的API网关与接口规范RESTful API设计:所有终端通过一套风格统一、语义清晰的RESTful API与后端交互。安全的身份认证:采用JWT(JSON Web Token)或无状态Session进行用户身份鉴权,保障接口安全。高效的数据同步:关键状态变更(如预约成功)通过WebSocket或轮询机制确保各端数据实时性。原文链接:https://blog.csdn.net/zhangyi2376775/article/details/155821733
  • [技术干货] 【Java】UDP网络编程:无连接通信到Socket实战
    1.什么是网络编程?网络编程:指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。要想进行网络编程,首先要学会操作系统给我们提供的一组API,我们通过这些API才能进行网络编程。这个API可以认为是应用层和传输层之间交互的路径。(我们只需要知道用户输入的是什么,然后调系统的API就可以完成网络通信)传输层提供的两个主要网络协议是TCP和UDP:这两个协议的原理差距很大,因此通过这两个协议进行网络编程的时候就存在一些差别。故系统提供了两组API供我们使用首先我们先了解一下两个协议大的方向的区别在哪里,具体细节我留到下一个博客进行讲解2.TCP和UDP的区别2.1.TCP是有连接的,UDP是无连接的(这里的连接是抽象的概念)这里连接的本质上是建立连接的双方各自保留对方的信息,两个计算机建立连接,就是彼此保留了对方的关键信息TCP想要通信,就需要先建立连接(保存对方信息)做完之后才能进行通信(如果A想要和B建立连接,B拒绝了,那么通信就没办法完成)UDP想要通信,就直接发送数据就可以了~~,不管你是否同意,UDP也不会保留对方的信息(UDP什么也不知道,但是我们程序员要知道,UDP自己不保存,但是我们发送数据肯定还是要把对方的IP和端口号都发送过去)2.2.TCP是可靠传输,UDP是不可靠传输在网络通信中,A会给B发送一个消息,B不可能100%收到但是可靠传输就是就算A的信息没有传输过去,A能知道,进一步在发送失败的时候采取一定的措施(就像微信发送消息没发送过去有一个红感叹号)TCP内置了可靠传输,UDP没有(后面我会详细讲解)(但是你可靠传输考虑的东西就太多了,效率就要牺牲,但是我们还是通过一些方法能补救回来一点)2.3.TCP是面向字节流的,UDP是面向数据报的TCP和文件操作一样都是以字节为单位进行传输的UDP是按照数据报(DatagramPacket)为单位进行传输(只能是数据报的整数倍)2.4.TCP和UDP都是全双工的3.UDP的Socket API如何进行使用?首先关于InetAddress这个类try {    InetAddress address = InetAddress.getByName("www.google.com");    System.out.println("IP Address: " + address.getHostAddress());} catch (UnknownHostException e) {    e.printStackTrace();}AI写代码结果:IP Address: 142.250.190.36InetAddress:用于表示 IP 地址的类,支持 IPv4 和 IPv6。getByName():用于解析主机名或 IP 字符串,返回InetAddress 对象。IP 地址作为参数:在网络编程中,IP 地址是定位目标设备的关键,因此需要作为参数传入相关方法(这里就需要把IP地址传入getByNAme()方法里面进行解析域名)。通过InetAddress和getByName(),Java 网络编程可以轻松处理 IP 地址和域名解析,简化了开发者的工作。UDP协议中两个API使用方法:Socket 在 Java 中的本质:是一个基于流的通信端点抽象,接收数据报的时候就会抛出IO异常,DatagramSocket 是 Java 中用于 UDP 通信的类。可以把它理解为一个 “邮筒”:邮筒的作用:你往邮筒里投递信件(数据包),邮递员(网络)会把信件送到目的地。DatagramPacket APIDatagramPacket是UDP Socket发送和接收的数据报的类DatagramPacket(byte[]buf, int length)构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数 length)DatagramPacket(byte[]buf, int offset, int length, SocketAddress address构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 4.这里我们将写一个简单的UDP客户端,服务器通信的程序,就简单的调用socket API(回显服务器)服务器在程序员手里,一个服务器上面都有哪些程序和端口是可控的。我们写代码的时候分配一个空闲的端口给服务器就行了但是客户可能都不知道端口是啥意思,万一把这个端口和其他程序的端口搞一起了就不妙了,我们还是直接让系统给客户分配一个的好4.1.服务器代码解释import com.sun.deploy.net.socket.UnixDomainSocket; import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.SocketException; public class UdpEchoServer {    private DatagramSocket socket = null;    private int port ;    // 服务器指定端口号    public UdpEchoServer(int port) throws SocketException {   // new一个Socket对象的时候会抛出这个异常        socket = new DatagramSocket(port);    }    // 服务器启动!!!(原神启动!!!)    public void start() throws IOException {   //Socket 在 Java 中的本质:是一个基于流的通信端点抽象,接收数据报的时候就会抛出IO异常        while(true){  // 服务器要一直运行            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);            //我们申请4096个字节的数据            socket.receive(requestPacket);//这是一个输出型参数            //当前完成这个receive之后,数据是以二进制的方式存储在DatagramPacket中的            // 如果我们想要把这里的数据显示出来,并且进行处理就需要把二进制数据转换成字符串            // 收到这个数据报就需要进行解析,转换成字符串,我们能够看懂的             String request = new String(requestPacket.getData(),0,requestPacket.getLength());            String response =  process(request); // 回显服务器,什么都不用干(相当于我们已经解析完成了)            //把响应写回客户端,肯定还是把数据报给写回去呀             DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,                    requestPacket.getSocketAddress());//   把字符串抓换成字节数组,以及字节数组的长度搞过去(老一套构建数据报的格式了            //但是还是要获取到来的数据报的地址呀,不然不知道你发给哪个客户             socket.send(responsePacket);            // 我们再打印一个日志,记录这次数据交互的详细情况!            System.out.printf("[%s : %d],req = %s,rep =  %s\n",requestPacket.getAddress().toString(),                    requestPacket.getPort(),request,response);            // 把IP地址,端口号打印出来,以及请求和响应        }        // 我们还要理解getSocketAddress和getAddress的区别,前者返回IP地址和端口号,后者只返回IP地址     }     public String process(String request){        return request;    }     public static void main(String[] args) throws IOException {        UdpEchoServer udpEchoServer = new UdpEchoServer(9090);        udpEchoServer.start();    } }AI写代码 有两个问题:问题一: 不行,这里面如果有中文字符的话字符串长度就不是字节长度了(UTF-8编码中文字符是3个字节,GBK编码里中文字符是2个字节)问题二:上述我写的代码里面为什么没有close?不写close不会文件资源泄露吗?import java.io.IOException;import java.net.*;import java.util.Scanner; public class UdpEchoClient {    /*    1. 创建一个Socket对象,发送接收数据报    2.我们初始化数据报的时候因为Udp是无连接的,因此我们需要把服务器的  IP和端口号都发送过去(两个成员变量)    3. 肯定要把我们的字符串(发送的本质内容转换成字节数组,然后一起构造成数据报DatagramPacket    4.把数据报发送出去    5.接收数据报(给数据包申请字节空间)    6.把数据报再转成字符串    * */    private DatagramSocket socket = null;    private String serverIP;    private int serverPort = 0;    public UdpEchoClient(String serverIP,int serverPort) throws SocketException {        socket = new DatagramSocket();        this.serverIP = serverIP;        this.serverPort = serverPort;    }     //客户端启动!!!    public void start() throws IOException {        System.out.println("客户端启动!!!");        Scanner scanner = new Scanner(System.in);        while(true){            String request = scanner.next();            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,                    InetAddress.getByName(serverIP),serverPort);  //  把IP端口号都发送过去            /*InetAddress:用于表示 IP 地址的类,支持 IPv4 和 IPv6。             getByName():用于解析主机名或 IP 字符串或者域名,返回 InetAddress 对象*/             //通过 InetAddress 和 getByName(),Java 网络编程可以轻松处理 IP 地址和域名解析,简化了开发者的工作。            socket.send(requestPacket);            //接收响应            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);            socket.receive(responsePacket);            // 把数据报里面的二进制数据转成我们能看懂的字符串            String response = new String(responsePacket.getData(),0,responsePacket.getLength());            System.out.println(response);        }    }      public static void main(String[] args) throws IOException {        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);        udpEchoClient.start();    }}AI写代码刚才这个程序在一个主机上面(并没有实现真正的跨主机通信的效果)服务器在我自己电脑上面,小明是访问不到的(除非小明和我在一个局域网里面)但是如果我把这个程序部署到云服务上面我们就可以实现互相通信的效果了(自己的电脑没有公网IP)原文链接:https://blog.csdn.net/2302_80639556/article/details/146409209
总条数:739 到第
上滑加载中