• [技术干货] Java 异常处理:从新手避坑到最佳实践
    一、为什么需要异常处理? 想象一下:你写的 Java 程序运行时突然因 “数组下标越界”“空指针” 崩溃,没有任何提示,用户根本不知道问题出在哪。异常处理的核心目的就是: 捕获程序运行时的错误,避免程序直接崩溃;清晰提示错误原因,便于定位和修复问题;保证程序在出错后仍能优雅地继续执行(或正常退出)。 二、Java 异常体系的核心结构 Java 中所有异常都继承自Throwable类,主要分为两大分支: Error(错误):JVM 级别的严重错误,如OutOfMemoryError、StackOverflowError,程序无法捕获和处理,只能通过优化代码 / 配置避免;Exception(异常):程序可处理的错误,又分为:受检异常(Checked Exception):编译时必须处理,如IOException、SQLException;非受检异常(Unchecked Exception):运行时异常,编译时无需处理,如NullPointerException、ArrayIndexOutOfBoundsException。  三、异常处理的核心语法:try-catch-finally 这是 Java 处理异常的基础语法,先看一个完整示例: java 运行    import java.io.FileReader;import java.io.IOException;public class ExceptionDemo { public static void readFile(String path) { FileReader fr = null; try { // 可能抛出异常的代码块 fr = new FileReader(path); int ch = fr.read(); System.out.println("读取到字符:" + (char) ch); } catch (IOException e) { // 捕获并处理异常 System.out.println("文件读取失败:" + e.getMessage()); e.printStackTrace(); // 打印异常堆栈,便于调试 } finally { // 无论是否抛出异常,都会执行的代码(常用于释放资源) try { if (fr != null) { fr.close(); // 关闭文件流 } } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { readFile("test.txt"); // 若文件不存在,会捕获IOException }}  关键说明: try:包裹可能抛出异常的代码,必须有;catch:捕获指定类型的异常,可多个(按异常子类到父类顺序);finally:可选,但常用于释放资源(如文件流、数据库连接),即使try/catch中有return,finally仍会执行。 四、Java 7+ 简化资源释放:try-with-resources 手动关闭资源容易遗漏,try-with-resources可以自动关闭实现了AutoCloseable接口的资源(如 FileReader、Connection): java 运行    public class TryWithResourcesDemo { public static void readFile(String path) { // 资源声明在try()中,执行完自动关闭 try (FileReader fr = new FileReader(path)) { int ch = fr.read(); System.out.println("读取到字符:" + (char) ch); } catch (IOException e) { System.out.println("文件读取失败:" + e.getMessage()); } } public static void main(String[] args) { readFile("test.txt"); }}  五、自定义异常:让异常更贴合业务场景 系统自带的异常无法满足业务需求时,可自定义异常。例如,用户登录时 “密码错误” 异常: java 运行    // 自定义受检异常(继承Exception)public class PasswordErrorException extends Exception { // 构造方法 public PasswordErrorException() { super(); } public PasswordErrorException(String message) { super(message); }}// 使用自定义异常public class LoginService { public static void login(String username, String password) throws PasswordErrorException { if (!"123456".equals(password)) { // 抛出自定义异常 throw new PasswordErrorException("密码错误,请重新输入"); } System.out.println("登录成功!"); } public static void main(String[] args) { try { login("admin", "111111"); } catch (PasswordErrorException e) { System.out.println("登录失败:" + e.getMessage()); } }}  六、异常处理的避坑指南(新手必看) 不要捕获所有异常(catch (Exception e)):会掩盖真正的错误,应精准捕获具体异常;不要空 catch 块:catch (Exception e) {}会让异常 “消失”,无法定位问题,至少要打印异常信息;不要滥用 throws:把异常抛给上层却不处理,最终会导致程序崩溃;finally 中避免返回值:会覆盖 try/catch 中的 return 结果,造成逻辑混乱。 七、总结 Java 异常体系分为 Error(不可处理)和 Exception(可处理),Exception 又分受检 / 非受检异常;try-catch-finally是基础异常处理语法,Java 7 + 的try-with-resources可自动释放资源;异常处理要精准、有意义,避免捕获所有异常或空 catch 块,自定义异常可贴合业务场景。 整体总结 第一篇文章聚焦 Java 核心类String,从不可变性、常量池、实战避坑三个维度讲解,适合初学者夯实基础;第二篇文章聚焦异常处理,从体系结构、核心语法、简化写法、自定义异常到避坑指南,覆盖新手到进阶的核心知识点;两篇文章均包含可直接运行的代码示例,兼顾原理讲解和实战应用,符合 Java 学习的认知逻辑。
  • [技术干货] 从原理到实践:彻底搞懂 Java 中的 String 类
    一、为什么 String 是 Java 中最特殊的类? 对于 Java 初学者来说,String类是接触最早、使用最频繁的类,但也是最容易踩坑的类。它的特殊性体现在三个核心点: 不可变性:String 对象一旦创建,其内部的字符序列就无法被修改;字符串常量池:Java 为 String 设计了常量池机制,用于节省内存、提高性能;重载的 + 运算符:String 是 Java 中唯一重载了 + 运算符的类,但其底层实现容易被误解。 二、String 的不可变性到底是什么? 先看一段简单的代码: java 运行    public class StringImmutableDemo { public static void main(String[] args) { String str = "Hello"; str = str + " Java"; // 看似修改了str,实则创建了新对象 System.out.println(str); // 输出:Hello Java } }  这段代码中,str = str + " Java"并不是修改了原来的 "Hello" 对象,而是创建了一个新的 String 对象 "Hello Java",并让 str 指向这个新对象。 String 的不可变性由其底层实现保证: java 运行    // String类的核心源码(简化版)public final class String { private final char value[]; // 存储字符的数组被final修饰 // 构造方法、方法等省略}  final修饰的String类:无法被继承,避免子类破坏不可变性;final修饰的value数组:数组引用无法被修改(但数组本身的元素理论上可通过反射修改,不推荐)。 三、字符串常量池:提升性能的关键 字符串常量池(String Pool)是 JVM 为 String 专门开辟的一块内存区域(位于堆中),核心作用是缓存字符串常量,避免重复创建。 1. 两种创建 String 对象的方式对比 java 运行    public class StringPoolDemo { public static void main(String[] args) { // 方式1:直接赋值,会先检查常量池,存在则复用,不存在则创建 String s1 = "Java"; String s2 = "Java"; System.out.println(s1 == s2); // true,指向常量池同一个对象 // 方式2:new关键字,会先在堆中创建新对象,常量池若没有则创建常量 String s3 = new String("Java"); String s4 = new String("Java"); System.out.println(s3 == s4); // false,堆中不同对象 System.out.println(s3.equals(s4)); // true,内容相等 // 手动入池:intern()方法 String s5 = s3.intern(); System.out.println(s1 == s5); // true,s5指向常量池对象 }}  四、String 常用操作避坑指南 避免频繁使用 + 拼接字符串:循环中使用+拼接会创建大量临时对象,推荐使用StringBuilder(单线程)或StringBuffer(多线程): java 运行    // 低效写法String result = "";for (int i = 0; i < 1000; i++) { result += i;}// 高效写法StringBuilder sb = new StringBuilder();for (int i = 0; i < 1000; i++) { sb.append(i);}String result = sb.toString();  equals 和 == 的区别:==比较对象地址,equals比较字符串内容,判断字符串相等务必用equals;空字符串判断:优先使用str.isEmpty()或str.length() == 0,避免str == ""(地址比较)。 五、总结 String 的不可变性是其核心特性,由final关键字和底层数组实现;字符串常量池可减少内存占用,直接赋值和new创建对象的逻辑不同;频繁拼接字符串优先用StringBuilder,避免不必要的性能损耗。
  • [技术干货] 开发者技术支持 - 从Java到ArkTS的迁移解决方案
    随着HarmonyOS NEXT全面转向ArkTS,大量存量Java应用与Android生态开发者面临核心技术栈重构的挑战。本方案旨在系统性解决Java开发者向ArkTS迁移过程中的核心痛点,提供可实操的路径指引。1.1 问题说明Java开发者在迁移过程中,通常在以下场景面临编译错误、运行时异常或性能/行为不一致问题:语法与类型不兼容:首次编译大量ArkTS代码时,出现“类型不匹配”“属性不存在”等静态检查错误。并发模型重构:原有依赖synchronized、ReentrantLock实现的多线程共享内存模型直接迁移后,出现数据错乱、程序崩溃。异步编程重构:Thread、Runnable、ExecutorService等代码无法直接运行,需适配异步模型。跨语言交互异常:与Java云服务端进行加密解密、签名验签等操作时,结果不一致。运行时行为差异:单例对象在不同线程(如StartupTask与EntryAbility)中并非同一实例;this指向异常导致程序崩溃。具体表现为:项目编译失败,控制台输出大量ArkTS语法错误码;应用运行时出现数据竞争、UI卡顿或跨平台交互失败。1.2 原因分析核心原因在于ArkTS与Java在设计哲学与运行时模型上存在本质差异。静态类型 vs 动态类型:ArkTS是强静态类型语言,禁止使用any/unknown,且不支持Java中“结构类型”(仅形状相似即可赋值)。Java的灵活类型转换在ArkTS中需显式声明。Actor并发模型 vs 共享内存模型:ArkTS采用内存隔离的Actor模型(通过TaskPool和Worker通信),不支持多线程直接读写同一对象。而Java的synchronized机制构建于共享内存之上,两者范式冲突。异步编程范式:Java通过多线程和Future处理并发,而ArkTS基于事件循环,使用Promise/async/await进行单线程异步调度,避免阻塞UI线程。数值与编码差异:数字精度:ArkTS的number基于IEEE 754双精度浮点数,仅能精确表示53位整数。与C++侧交互64位大整数(如指针)时,需使用BigInt。字节符号:ArkTS的Uint8Array为无符号字节(0-255),而Java的byte为有符号(-128-127)。加解密、哈希等操作若直接传输原始字节数组,会导致云侧解密/验签失败。运行时上下文(this)绑定:Java中this在编译时确定,指向当前实例。ArkTS中this是动态绑定的,取决于调用时的上下文,若将对象方法作为回调传递,可能导致this指向错误而崩溃。1.3 解决思路解决问题的核心是“范式转换”,而非简单的语法翻译。总体遵循以下优化方向:类型显式化与结构定义化:将模糊的Java类型(尤其是Object)替换为ArkTS明确的接口、类或联合类型,杜绝使用any。并发任务化与通信消息化:将“共享内存+锁”的线程逻辑,拆解为独立的可序列化任务,通过TaskPool分发,或通过Worker进行消息通信。共享数据需通过@Sendable类或SharedArrayBuffer传递。异步包装与Promise化:将阻塞式或后台线程逻辑,重构为async函数,利用TaskPool执行CPU密集型任务。数据格式中间层对齐:与Java端进行二进制数据交互时,必须主动进行符号转换(Uint8Array <-> Int8Array)或使用标准格式(如Base64)作为中间层。上下文绑定显式化:传递对象方法时,使用.bind(this)或箭头函数固定this指向。1.4 解决方案方案一:并发模型迁移(共享内存 -> 内存隔离)场景:将后台计算任务移至子线程。使用@Concurrent装饰函数import { taskpool } from '@kit.ArkTS'; // 原Java: new Thread(() -> { calculate(); }).start(); @Concurrent function calculate(data: number): number { // 执行独立计算,注意不能访问外部非Sendable变量 return data * 2; } async function doTask() { let task = new taskpool.Task(calculate, 42); // 创建任务 let result = await taskpool.execute(task); // 执行并等待结果 console.info(`Result: ${result}`); }  使用@Sendable类传递对象import { taskpool } from '@kit.ArkTS'; @Sendable // 标记为可跨线程传递 class CalculationTask { private factor: number = 2; run(input: number): number { return input * this.factor; } } @Concurrent function runner(task: CalculationTask, value: number): number { return task.run(value); } async function doComplexTask() { let myTask = new CalculationTask(); let task = new taskpool.Task(runner, myTask, 21); let result = await taskpool.execute(task); }  方案二:异步编程迁移(Thread -> Promise)场景:网络请求等I/O操作。 // 原Java: new Thread(() -> { httpRequest(); }).start(); async function fetchData(): Promise<void> { try { let response = await http.request('https://api.example.com/data'); // 假设的API let data = response.result as string; // 更新UI (会自动回到UI线程) this.uiData = data; } catch (error) { console.error('Request failed:', error); } } // 在UI事件中直接调用: fetchData();  方案三:跨语言数据兼容性处理场景:HMAC-SHA1加密,保证ArkTS结果Java可解密。 import { cryptoFramework } from '@kit.CryptoArchitecture'; import { util } from '@kit.ArkTS'; async function hmacSha1(key: string, data: string): Promise<Int8Array> { // ... 省略加密步骤,得到 Uint8Array 结果 ... let uint8Result: Uint8Array = await doHmacSha1(key, data); // *** 关键转换:将无符号字节数组转换为有符号字节数组 *** let int8Result = new Int8Array(uint8Result.length); for (let i = 0; i < uint8Result.length; i++) { let value = uint8Result[i]; // 将 >127 的值转换为负数表示 int8Result[i] = value > 127 ? value - 256 : value; } // 也可以使用更简洁的方式: let int8Result = new Int8Array(uint8Result); // 通常再转换为Base64字符串传输 let base64Str = util.Base64.encodeToString(int8Result); return int8Result; }  Java服务端需使用byte[]接收,并对Base64字符串进行解码。方案四:解决this绑定问题 class MyClass { private name: string = 'MyClass'; handleClick() { console.info(this.name); } registerCallback() { // 错误:直接传递方法,this会丢失 // someComponent.setCallback(this.handleClick); // 正确:使用箭头函数或bind绑定this someComponent.setCallback(() => this.handleClick()); // 或 someComponent.setCallback(this.handleClick.bind(this)); } }  方案五:单例模式适配(跨线程)由于ArkTS线程内存隔离,要实现真正的进程内单例,需借助共享模块(HSP)。将单例类定义在HSP模块中。确保类被标记为@Sendable(如果需要在线程间传递)。// 在HSP模块中定义 @Sendable export class GlobalDataManager { private static instance: GlobalDataManager; private data: Map<string, Object> = new Map(); private constructor() {} public static getInstance(): GlobalDataManager { if (!GlobalDataManager.instance) { GlobalDataManager.instance = new GlobalDataManager(); } return GlobalDataManager.instance; } // ... 其他方法 ... }  在不同模块或线程中,通过GlobalDataManager.getInstance()访问的是HSP提供的唯一实例。1.5 结果展示通过实施本解决方案:成功编译与运行:项目可顺利升级至compatibleSdkVersion ≥ 10的标准模式,通过严格的ArkTS语法检查,构建出符合NEXT标准的应用。稳定性与性能提升:基于Actor模型的并发设计从根本上避免了数据竞争和死锁,提高了应用稳定性。静态类型检查和优化的异步模型减少了运行时开销,提升了性能。生态无缝对接:通过规范的数据转换(如字节符号处理),确保ArkTS应用与既有Java服务端生态(如加解密、签名)的无缝、正确交互。提供标准范式:为团队后续所有Java向ArkTS的迁移项目提供清晰的、经过验证的架构改造指南和代码范例,大幅降低迁移过程中的试错成本和学习曲线,将迁移效率提升50%以上。结论:从Java到ArkTS的迁移是一次从“语言”到“范式”的升级。理解并接受内存隔离、事件驱动、强静态类型等新范式,是成功迁移的关键。本方案提供的系统性思路和具体“配方”,能有效引导开发者跨越鸿沟,构建更稳健、高性能的HarmonyOS应用。
  • [技术干货] 【Java】2025 年 Java 学习路线:从入门到精通
    Java作为一门拥有近30年历史的编程语言,凭借其 跨平台特性、丰富的生态系统 和 广泛的应用场景,始终占据编程语言排行榜前列。面对云原生、微服务、AI等技术的蓬勃发展,Java也在持续进化(如虚拟线程、GraalVM)。本文将为初学者和进阶者梳理一条清晰的2025年Java学习路线,涵盖 基础语法到云原生微服务架构 的全流程,并提供实用的学习资源推荐。一、Java基础阶段(4-8周)1. 开发环境搭建    JDK安装:深入理解JDK/JRE/JVM的关系,掌握环境变量(如JAVA_HOME, PATH)配置。推荐JDK 17或21(LTS)。    IDE使用:IntelliJ IDEA (社区版) 是首选,熟练掌握快捷键、调试(Debugging)、版本控制集成。    第一个Java程序:Hello World,理解编译(javac)、运行(java)过程及JVM作用。2. 核心语法基础    数据类型:严格区分基本类型(Primitive)与引用类型(Reference)。    流程控制:if-else, switch表达式(Java 12+), for/while/do-while循环。    数组与字符串:数组操作,String API 核心方法,StringBuilder/StringBuffer区别。    方法:定义、参数传递(值传递)、重载(Overload)、可变参数(Varargs)。3. 面向对象编程(OOP)    类与对象:理解面向对象的基本抽象单元。    四大基石:        封装 (Encapsulation):访问控制修饰符(private, protected, public)。        继承 (Inheritance):extends关键字,方法重写(Override),super。        多态 (Polymorphism):向上转型、向下转型、动态绑定。        抽象 (Abstraction):抽象类(abstract class)、接口(interface)。    接口 vs 抽象类:深刻理解应用场景与设计考量。    内部类:成员内部类、静态内部类、局部内部类、匿名内部类(Lambda前身)。4. 核心类库 (Java SE API)    集合框架 (Collections Framework):        核心接口:List (ArrayList, LinkedList), Set (HashSet, TreeSet), Map (HashMap, LinkedHashMap, TreeMap)。        HashMap 原理(哈希表、冲突解决、扩容)是面试重点。        迭代器(Iterator)、比较器(Comparator/Comparable)。    异常处理 (Exception Handling):        异常体系:Throwable -> Error/Exception(Checked/Unchecked)。        try-catch-finally / try-with-resources (Java 7+)。        自定义异常。    IO/NIO:        字节流(InputStream/OutputStream)、字符流(Reader/Writer)。        File类、序列化(Serializable)。        NIO 基础概念:Buffer, Channel, Selector。    多线程 (Multithreading):        创建线程:继承Thread vs 实现Runnable/Callable + Future。        线程池:ExecutorService 及其实现(ThreadPoolExecutor)。        线程同步:synchronized关键字、Lock接口(ReentrantLock)及其条件变量。        线程通信:wait()/notify()/notifyAll()。    反射 (Reflection):        Class对象获取。        动态创建对象、访问字段/方法。        动态代理(Proxy)。5. 关联技术基础理解Web开发全貌所需,不必精通,但需知晓概念与交互。前端三剑客 (HTML, CSS, JavaScript):    HTML:基础结构、常用标签。    CSS:基础样式、选择器、盒模型。    JavaScript (JS):基础语法、DOM操作、事件处理、Ajax (Fetch API)。    主流前端框架:了解Vue.js / React.js 的作用和基本概念。拓展阅读:HTML 与 CSS 基础教程Web基础协议:    HTTP/HTTPS:请求/响应结构、方法(GET/POST/PUT/DELETE)、状态码、Header、Cookie。    TCP/IP:了解OSI模型、TCP三次握手/四次挥手。会话管理技术:    Cookie & Session:原理、区别、应用场景。    JWT (JSON Web Token):现代无状态会话方案。拓展阅读:会话技术:Cookie 与 Session 详解推荐资源:    书籍:《Java核心技术 卷Ⅰ》(Core Java Volume I) - 经典权威。    视频:B站【狂神说Java】基础篇 - 通俗易懂。    练习:LeetCode简单题目(两数之和、反转链表等),牛客网Java基础题。二、Java 进阶阶段(6-10周)1. JVM 深度理解    内存模型 (Runtime Data Areas):堆(Heap)、栈(Stack/VM Stack/Native Method Stack)、方法区(Metaspace)、程序计数器(PC Register)。    垃圾回收 (GC):        对象存活判断(引用计数、可达性分析)。        GC算法:标记-清除、标记-整理、复制;分代收集思想(Young/Old)。        垃圾收集器:Serial, Parallel, CMS, G1, ZGC, Shenandoah。掌握G1原理与调优。        GC日志分析。    类加载机制:        过程:加载 -> 链接(验证、准备、解析) -> 初始化。        双亲委派模型 (Parents Delegation Model) 及其打破。        类加载器:Bootstrap, Extension/Platform, Application。    JVM调优实战:        常用JVM参数 (-Xms, -Xmx, -XX:NewRatio, -XX:SurvivorRatio, -XX:+UseG1GC等)。        使用工具:VisualVM, JConsole, jstat, jmap, jstack 分析内存、线程、GC。2. 并发编程 - 应对高并发挑战    线程池进阶:        ThreadPoolExecutor 核心参数(corePoolSize, maxPoolSize, workQueue, keepAliveTime, handler)详解。        线程池状态流转。        合理配置线程池。    并发工具类 (java.util.concurrent):        CountDownLatch:等待多个任务完成。        CyclicBarrier:线程到达屏障点等待。        Semaphore:控制并发访问数。        Exchanger:线程间交换数据。    原子类 (java.util.concurrent.atomic):        AtomicInteger, AtomicReference等。        CAS (Compare-And-Swap) 原理 及 ABA 问题。    volatile 关键字:保证可见性、禁止指令重排序(部分),理解其局限性。    并发容器:ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue(ArrayBlockingQueue, LinkedBlockingQueue)。3. Java新特性 - 拥抱现代化    Lambda表达式:语法、作用域。    函数式接口 (Functional Interface):@FunctionalInterface, java.util.function包 (Predicate, Function, Consumer, Supplier)。    Stream API:流式操作 (filter, map, reduce, collect)、并行流。    模块化系统 (Java Platform Module System - JPMS, Java 9+):module-info.java,理解模块化带来的变化。    记录类 (Record, Java 16):简化不可变数据载体。    模式匹配 (Pattern Matching, Java 17 preview, 后续版本增强):instanceof模式匹配、switch模式匹配(预览特性,关注进展)。4. 设计模式    创建型:单例模式(多种实现及适用场景)、工厂方法模式、抽象工厂模式、建造者模式。    结构型:适配器模式、装饰器模式、代理模式(静态、JDK动态、CGLIB)、外观模式。    行为型:策略模式、观察者模式、责任链模式、模板方法模式。    理解Spring等框架中设计模式的应用(如IoC容器是工厂模式,AOP是代理模式)。推荐资源:    书籍:《Effective Java (3rd)》 - Java编程最佳实践圣经,《Java并发编程实战》 - 并发领域权威。    工具:JProfiler (付费强大), VisualVM, Arthas (阿里开源在线诊断工具)。    实践:动手实现简易线程池、内存泄漏检测Demo、应用设计模式重构小项目。三、数据库与MySQL(2-3周)1. 环境搭建    MySQL 8.x安装与配置:掌握Linux/Windows下安装,理解关键配置项。    客户端工具:熟练使用MySQL Workbench (官方)、Navicat Premium (流行商业工具)。    字符集与排序规则:统一使用utf8mb4和utf8mb4_0900_ai_ci (或合适校对规则) 避免乱码。2. SQL核心与进阶拓展阅读:MySQL 基础入门指南    SQL深度掌握:        DDL (数据定义语言):CREATE/ALTER/DROP (DATABASE, TABLE, INDEX)。        DML (数据操作语言):INSERT/UPDATE/DELETE。        DQL (数据查询语言):SELECT语句精髓,聚合函数(COUNT, SUM, AVG, MAX, MIN, GROUP_CONCAT),分组(GROUP BY),过滤(HAVING)。        DCL (数据控制语言):GRANT/REVOKE。        复杂查询:深入理解7种JOIN (INNER, LEFT/RIGHT [OUTER], FULL[OUTER], CROSS, SELF),子查询优化技巧。        窗口函数 (Window Functions):ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE(), 聚合函数+OVER子句。解决复杂分析问题利器。        存储过程与函数:开发流程,理解其优缺点(维护性、性能)并谨慎使用。        触发器 (Triggers):原理,极其谨慎使用。3. 数据库设计与性能优化拓展阅读:MySQL 进阶知识数据库设计:    范式理论:1NF/2NF/3NF/Boyce-Codd NF (BCNF) 的理解与权衡,反范式设计优化查询性能。    ER建模:使用工具(如MySQL Workbench, PowerDesigner)设计表结构及关系。    索引优化:        B+树原理是理解索引的基石。        索引类型:主键、唯一、普通、全文、空间、组合索引。        最左前缀原则是组合索引生效的关键。        索引覆盖、索引下推 (ICP)。        何时建索引?索引失效场景排查(函数、运算、类型转换、OR条件、!=/<>、LIKE通配符开头)。    执行计划 (EXPLAIN):必须精通解读 type, key, rows, Extra 等字段含义。    慢查询日志:开启、分析(mysqldumpslow, pt-query-digest)、优化。事务与锁机制:    ACID特性:理解InnoDB如何通过redo log(重做日志)保证持久性(D)、undo log(回滚日志)保证原子性(A)和一致性©、锁和MVCC保证隔离性(I)。    隔离级别 (Isolation Levels):        READ UNCOMMITTED -> 脏读        READ COMMITTED -> 不可重复读        REPEATABLE READ (MySQL默认) -> 幻读 (InnoDB通过Next-Key Locking解决)        SERIALIZABLE    锁机制:共享锁(S锁)、排他锁(X锁)、意向锁(IS/IX)。行锁、间隙锁(Gap Lock)、临键锁(Next-Key Lock)、死锁检测与处理 (SHOW ENGINE INNODB STATUS)。    MVCC (Multi-Version Concurrency Control):ReadView、undo log版本链,理解REPEATABLE READ和READ COMMITTED下可见性规则差异。高可用与扩展架构:    主从复制 (Replication):原理(基于binlog)、配置(异步/半同步)、GTID模式。    读写分离:应用层实现、中间件实现(ShardingSphere-JDBC, MyCat)。    分库分表 (Sharding):        垂直拆分、水平拆分。        分片策略:Range(范围)、Hash(哈希)、取模、一致性Hash。        中间件:ShardingSphere (Apache顶级项目,生态完善)、MyCat。    高可用集群 (HA):MHA (Master High Availability), MGR (MySQL Group Replication, InnoDB Cluster基础)。JDBC (Java Database Connectivity):理解驱动、Connection、Statement/PreparedStatement (防SQL注入)、ResultSet、事务管理。 拓展阅读:JDBC 讲解全面教程推荐资源:    书籍:《高性能MySQL(第4版)》    官方文档:MySQL 8.0 Reference Manual    在线练习:LeetCode数据库题库、SQLZoo、HackerRank SQL。    工具:pt-query-digest (Percona Toolkit), MySQL Workbench Performance Reports。四、开发框架与中间件(8-12周)1. Spring 生态    Spring Framework Core:        IoC (控制反转) / DI (依赖注入):核心思想,ApplicationContext, Bean生命周期、作用域(singleton, prototype等)、配置方式(XML, Java Config, Annotation)。        AOP (面向切面编程):核心概念(切面Aspect、连接点Joinpoint、通知Advice、切入点Pointcut),实现原理(动态代理),常用场景(日志、事务、安全)。    Spring MVC:        核心组件:DispatcherServlet, HandlerMapping, Controller, ViewResolver。        请求处理流程。        RESTful API 设计规范与实践,常用注解 (@RestController, @RequestMapping, @GetMapping等)。    Spring Boot:        核心理念:约定优于配置、自动配置 (@EnableAutoConfiguration, spring.factories)、嵌入式容器(Tomcat, Jetty, Undertow)。        Starter:理解机制,常用Starter (spring-boot-starter-web, spring-boot-starter-data-jpa, spring-boot-starter-data-redis, spring-boot-starter-security, spring-boot-starter-actuator)。        外部化配置 (application.properties/.yml), Profile, 日志配置。        打包 (spring-boot-maven-plugin) 与运行。    Spring Cloud (构建分布式/微服务系统):        服务注册与发现:Nacos (推荐,阿里开源,功能丰富), Eureka (逐步淡出)。        配置中心:Nacos Config, Spring Cloud Config Server。        服务调用:OpenFeign (声明式REST客户端)。        负载均衡:Spring Cloud LoadBalancer (替代Ribbon)。        服务熔断与降级:Sentinel (推荐,阿里开源), Hystrix (维护模式)。        API网关:Spring Cloud Gateway (高性能, 异步), Netflix Zuul (1.x维护模式, 2.x不活跃)。        分布式链路追踪:Sleuth + Zipkin / SkyWalking。2. 持久层框架    MyBatis (半自动ORM, 灵活性强):拓展阅读:MyBatis 使用详解        XML映射文件:<select>, <insert>, <update>, <delete>, 动态SQL (<if>, <choose>, <foreach>, <where>, <set>, <trim>)。        核心对象:SqlSessionFactory, SqlSession。与Spring整合:SqlSessionTemplate。        缓存机制:一级缓存(Session级别)、二级缓存(Mapper/Namespace级别)配置与注意事项。        插件开发:原理(拦截器Interceptor),示例(如PageHelper分页实现)。        逆向工程:MyBatis Generator / MyBatis-Plus (国内流行,提供强大CRUD封装和代码生成)。    Spring Data JPA (基于JPA标准的全自动ORM, 开发效率高):        JPA (Java Persistence API) 规范,Hibernate 是其最流行实现。        核心概念:Entity, EntityManager, Repository (JpaRepository及其方法命名查询)。        关联关系映射:@OneToOne, @OneToMany/@ManyToOne, @ManyToMany。        查询方式:JPQL (Java Persistence Query Language), Criteria API (类型安全), 方法名推导。        N+1 查询问题:原因分析,解决方案(@EntityGraph, JOIN FETCH in JPQL)。        乐观锁:@Version注解实现并发控制。3. 常用中间件    缓存 (Cache):        Redis:绝对主流选择。数据类型(String, Hash, List, Set, Sorted Set, Bitmaps, HyperLogLog, Geospatial, Stream)。持久化(RDB, AOF)。主从复制、哨兵(Sentinel)、集群(Cluster)。客户端(Jedis, Lettuce, Redisson - 分布式对象和锁)。    消息队列 (Message Queue):        核心概念:生产者(Producer)、消费者(Consumer)、Broker、主题(Topic)、队列(Queue)。        Kafka:高吞吐、分布式、持久化、分区(Partition)、副本(Replica)、ISR机制、可靠性保证(ACK机制)。适用日志、大数据流处理。        RocketMQ (阿里开源):金融级稳定性、顺序消息、事务消息、延时消息。国内广泛应用。        RabbitMQ:基于AMQP协议,功能丰富(Exchange, Binding, Routing Key),消息确认机制灵活。    RPC框架 (Remote Procedure Call):        Dubbo (阿里开源):高性能Java RPC框架。核心:服务注册发现(Zookeeper, Nacos)、负载均衡、集群容错、服务治理。推荐工具链:    构建工具:Maven (主流) / Gradle (更灵活,Android/Kotlin首选)。掌握依赖管理、生命周期、插件使用。    版本控制:Git 是绝对标准。掌握基本命令(clone, add, commit, push, pull, branch, merge, rebase)、理解工作流(Feature Branching, Git Flow, GitHub Flow)。    容器化:Docker:核心概念(镜像Image、容器Container、仓库Repository)、基本命令(run, build, push, pull, ps, logs)、Dockerfile编写。Docker Compose:定义和运行多容器应用。五、项目实战与面试准备将所学知识融会贯通,构建简历亮点,针对性攻克面试难关。1. 项目实战 (选择1-2个深度打磨)    初级项目 (夯实基础):        电商系统核心模块:            商品中心:SPU/SKU设计、类目管理、商品搜索(SQL LIKE/简单ES)。            用户中心:注册登录、认证授权 (Authentication & Authorization)。            购物车:数据结构设计(Redis/DB)。            订单系统:状态机设计、下单流程、事务管理 (@Transactional)、库存扣减。            支付模块:模拟对接(支付宝/微信沙箱)。            性能关注点:商品列表页SQL优化 (EXPLAIN), 热点数据缓存(Redis)。        博客/内容管理系统 (CMS):            文章管理:CRUD、富文本编辑(集成UEditor/Kindeditor)、分类标签(多对多关系)。            评论系统:设计(嵌套评论/楼层评论)、审核。            用户与权限:角色管理、内容权限控制。            数据统计:访问量、热门文章。            进阶点:全文检索集成 (Elasticsearch / Solr)。    进阶项目 (挑战难点, 应对面试):        秒杀/高并发系统:            核心挑战:瞬时高并发、超卖、数据库压力。            解决方案:                分层校验:活动校验、用户资格校验前置。                缓存预热:活动信息、库存加载到Redis。                库存扣减:Redis Lua脚本保证原子性、异步扣减DB库存。                请求削峰:MQ(如RocketMQ/Kafka)排队处理。                限流熔断:网关层/应用层限流(Sentinel/Guava RateLimiter)、熔断降级。                页面静态化:CDN加速。            技术栈:Spring Boot + Redis + MQ + (Sentinel) + (ShardingSphere-JDBC)。        微服务实践:            服务拆分:按业务域(用户服务、商品服务、订单服务、支付服务)。            技术栈:Spring Boot + Spring Cloud Alibaba (Nacos, Sentinel, Seata, RocketMQ) / Spring Cloud Netflix (Eureka, Hystrix, Zuul) + MyBatis/Spring Data JPA。            核心问题解决:                服务治理:注册发现(Nacos)、负载均衡(LoadBalancer)、熔断降级(Sentinel)。                分布式事务:Seata AT模式 (2PC)、可靠消息最终一致性 (基于MQ)、TCC模式。理解优缺点。                数据一致性:分库分表 (ShardingSphere-JDBC)、数据同步(Canal监听MySQL binlog到ES/Redis)、分布式ID生成(Snowflake, Leaf)。                配置管理:Nacos Config。                链路追踪:Sleuth + Zipkin。            部署:Docker容器化。2. 面试专项准备    Java核心:        HashMap (1.7 vs 1.8+ 结构、put流程、扩容、线程安全ConcurrentHashMap)、ArrayList vs LinkedList。        JVM:内存区域、GC算法与收集器(G1重点)、类加载、OOM排查、JVM参数调优。        并发:线程状态、线程池原理与参数、synchronized/Lock/volatile/CAS、并发容器、ThreadLocal、AQS原理(理解ReentrantLock/CountDownLatch等实现基础)。        新特性:Lambda、Stream API、Optional、模块化、Record。    数据库 (MySQL 为主):        索引:B+树、聚簇/非聚簇索引、覆盖索引、最左前缀、索引失效场景、索引优化。        事务:ACID、隔离级别与问题、MVCC原理(ReadView)、锁机制(行锁、间隙锁、临键锁、死锁)、redo log/undo log/binlog作用与两阶段提交(2PC)。        性能优化:EXPLAIN解读、慢查询优化、分库分表方案(ShardingSphere)、主从复制与读写分离、高可用(MHA/MGR)。        设计:三范式、ER图、大表优化策略。    框架 (Spring为主):        Spring:IoC/DI原理、Bean生命周期、AOP原理与应用、事务传播行为与隔离级别、Spring MVC流程。        Spring Boot:自动配置原理、Starter原理、常用注解。        Spring Cloud:核心组件作用与原理(Nacos注册发现配置、Feign、Gateway、Sentinel、Seata)、服务治理、熔断降级策略、分布式事务方案对比。        ORM:MyBatis #{} vs ${}、缓存、动态SQL; JPA/Hibernate N+1问题、懒加载、关联映射。    中间件:        Redis:数据类型与应用场景、持久化、过期策略、内存淘汰策略、集群模式、分布式锁实现(setnx + lua)、缓存穿透/击穿/雪崩解决方案。        MQ (Kafka/RocketMQ):架构设计(Broker/Topic/Partition)、可靠性保证(ACK、副本)、顺序消息、事务消息、积压处理。    分布式:CAP/BASE理论、分布式ID、分布式锁、分布式Session、一致性协议(Raft/Paxos了解概念)。    系统设计:设计一个短链系统、设计一个抢红包系统、设计一个朋友圈/微博Feed流。掌握常见设计模式应用场景。    算法与数据结构:        基础:数组、链表、栈、队列、哈希表、堆、二叉树(遍历、BST、AVL/红黑树概念)、图(遍历、最短路径概念)。        算法:排序(快排、归并、堆排)、二分查找、DFS/BFS、递归、滑动窗口、双指针、动态规划(经典背包、路径问题)、贪心。        复杂度分析 (Big O Notation)。        刷题:LeetCode (按标签/公司/频率刷)、剑指Offer、牛客网。重点:链表、树、数组、字符串、动态规划、回溯。六、前沿探索、持续学习以及资源分享1. 前沿技术技术日新月异,保持好奇心和学习动力是关键。    云原生 (Cloud Native):        Kubernetes (K8s):容器编排事实标准,学习Pod、Deployment、Service、Ingress等核心概念。        Service Mesh:Istio / Linkerd,理解Sidecar模式、服务治理下沉。        Serverless:FaaS (Function as a Service) 概念与实践(AWS Lambda, 阿里云函数计算)。        云数据库:AWS RDS, Azure SQL Database, 阿里云PolarDB的优化与管理。    大数据基础:        Hadoop:HDFS (分布式存储), MapReduce (计算模型), YARN (资源调度)。        Spark:内存计算引擎,比MapReduce更高效,学习RDD/DataFrame/Dataset API。        流处理:Spark Streaming, Flink (低延迟、高吞吐)。    前沿Java技术:        Quarkus:为GraalVM和Kubernetes量身定制的超音速亚原子Java框架(启动快、内存小)。        GraalVM:高性能跨语言运行时,支持AOT编译(提前编译成本地镜像)。        Project Loom (虚拟线程 - Java 19+):轻量级线程,旨在显著提高高并发应用吞吐量,简化并发编程(java.lang.VirtualThread)。        Project Panama (外部函数与内存API - incubating):改进Java与原生代码(特别是C库)的互操作性。        Project Valhalla (值对象与泛型特化 - in progress):引入值类型(inline class),优化内存布局和性能。    数据库扩展:        时序数据库 (TSDB):InfluxDB, TimescaleDB,物联网(IoT)、监控场景应用。        图数据库 (Graph Database):Neo4j,处理复杂关系。    架构演进:深入理解DDD(领域驱动设计)、CQRS(命令查询职责分离)、Event Sourcing(事件溯源)、微服务治理最佳实践、可观测性(Observability - Metrics, Logging, Tracing)。2. 学习建议与资源    代码量是硬道理:        初期:保证每天200+行有效代码,动手敲,不要只看。        中后期:通过项目持续积累,目标:1-2个完整项目(含文档)写在简历上。    构建知识体系:        使用思维导图(XMind, MindMaster)或 双链笔记 (Obsidian, Logseq) 梳理技术脉络和关联。        定期回顾总结,形成自己的技术博客/笔记库。    善用优质资源:        官方文档永远是第一手资料(JDK, Spring, MySQL, Redis等)。        经典书籍(见各阶段推荐)。        高质量视频教程(B站、慕课网、极客时间)。        技术博客/社区:掘金、InfoQ、开发者头条、Stack Overflow (解决问题)、GitHub (学习源码、参与开源)、CSDN(精选文章)。    积极参与社区:        GitHub:Star优质项目,尝试提交PR修复小issue,阅读源码。        技术论坛:提问、回答问题、参与讨论。        线下/线上技术沙龙、Meetup。    避免常见误区:        不要过早陷入框架细节:先打好Java核心、数据结构算法、计算机网络、操作系统基础。        重视单元测试:熟练使用JUnit 5 / TestNG,培养TDD/BDD思维。        理解设计原则:SOLID (单一职责、开闭、里氏替换、接口隔离、依赖倒置) 比死记设计模式更重要。        关注原理,不止于会用:理解框架、中间件、JVM背后的工作原理。        英语能力很重要:大量一手资料、官方文档、前沿技术资讯都是英文。推荐资源汇总:    书籍:        基础/核心:《Java核心技术 卷I》《Effective Java》《Java编程思想》        并发:《Java并发编程实战》        JVM:《深入理解Java虚拟机》        数据库:《高性能MySQL》《SQL必知必会》《Redis设计与实现》        设计模式:《Head First设计模式》《设计模式:可复用面向对象软件的基础》        架构/Spring:《Spring实战》《Spring Boot实战》《微服务架构设计模式》    在线平台:        学习:B站、慕课网、极客时间、Coursera/edX (CS基础)        刷题:LeetCode、牛客网、LintCode        问答:Stack Overflow、SegmentFault        社区/博客:掘金、InfoQ、开发者头条、美团技术团队、阿里技术    文档:        Oracle Java Documentation        Spring Framework Documentation        Spring Boot Reference Documentation        MySQL Reference Manual        Redis Documentation        Kafka Documentation        Nacos Documentation    工具:        IDE: IntelliJ IDEA (Ultimate/Community)        构建: Maven, Gradle        版本控制: Git (GitHub, GitLab, Gitee)        数据库: MySQL Workbench, DBeaver, Navicat        接口测试: Postman, Insomnia        容器: Docker, Docker Desktop        监控/诊断: VisualVM, JConsole, Arthas, JProfiler (付费)Java 学习是一场马拉松而非短跑。 技术栈会更新,但扎实的基础、持续学习的热情、解决问题的能力和工程化的思维是长久立足之本。保持耐心,循序渐进,动手实践是掌握编程的唯一捷径。记住:The best time to start was yesterday. The next best time is now. (开始的最佳时间是昨天,其次是现在!) 踏上你的Java精进之旅吧!————————————————原文链接:https://blog.csdn.net/m0_64355285/article/details/155104433
  • [技术干货] Java 继承
    什么是继承在 Java 中,使用类来对现实中的实体进行描述,例如定义一个 Cat 类来描述猫:public class Cat {    String name;    int age;    public void eat() {        System.out.println("吃饭");    }}一键获取完整项目代码java定义一个 Dog 类来描述狗:public class Dog {    String name;    int age;    public void eat() {        System.out.println("吃饭");    }}一键获取完整项目代码java而上述代码中的 name、age 以及 eat 方法,都是重复的此时,就可以对这些共性进行抽取而在面向对象思想中提出了 继承 的概念,专门用来进行共性抽取,实现代码复用继承(inhertance):是面向对象编程的特征,它允许在保持原有类的基础上进行扩展,增加新的功能,这样产生的新类,称之为派生类。通过继承,能够实现共性的抽取,从而实现代码的复用我们将上述 Cat 和 Dog 的共性进行抽取,使用继承的思想来达到复用效果: 如上图所示,Dog 和 Cat 都继承了 Animal 类,其中,Animal 称为 父类(或 超类、基类),Dog 和 Cat 称之为 Animal 的 子类(或 派生类),继承之后,子类可以复用父类中的成员,子类在实现时只需关心自己新增的成员即可继承的语法在 Java 中若要表示类之间的继承关系,需要使用 extends 关键字:修饰符 class 子类 extends 父类{        ...}public class Animal {    String name;    int age;     public void eat() {        System.out.println("吃饭");    }}一键获取完整项目代码javaCat 类继承 Animal,还可以新增需要的成员变量或方法public class Cat extends Animal{    public void miaow() {        System.out.println("喵喵叫");    }}一键获取完整项目代码java子类会将父类中的成员变量或成员方法继承到子类中子类在继承父类之后,可以添加自己特有的成员父类成员的访问子类将父类中的方法和变量继承之后,那么,子类中能否直接访问父类中继承下来的成员呢?访问父类的成员变量当子类和父类不存在同名对象时public class A {    int a = 10;}一键获取完整项目代码javapublic class B extends A {    int b = 20;     public static void main(String[] args) {        B b = new B();        System.out.println(b.a);        System.out.println(b.b);        // System.out.println(b.c); // 编译失败,子类和父类中都不存在 c         b.method();    }}一键获取完整项目代码java运行结果:子类自己有就访问自己的成员变量,若没有,就访问父类的成员变量 当子类和父类存在同名对象时public class A {    int a = 10;    int b = 20;}一键获取完整项目代码javapublic class B extends A {    int a = 100; // 与父类中的成员同名,且类型相同    char b = 'b'; // 与父类中的成员同名,但类型不同    public void method() {        System.out.println("a: " + a + " b: " + b);    }     public static void main(String[] args) {        B b = new B();        b.method();    }}一键获取完整项目代码java运行结果:存在同名变量时,优先访问子类的成员变量  从上述两个示例中,可以看出:若访问的成员变量子类中有,则访问子类自己的成员变量若访问的成员变量子类中没有,则访问父类中继承下来的,若父类中也没有,则会编译报错若访问的成员变量,父类子类中都有,则优先访问自己的成员变量即 自己有就优先自己的,若没有再向父类中找访问父类的成员方法成员方法名不同public class A {    int a = 10;    int b = 20;    public void methodA() {        System.out.println("methodA...");    }}一键获取完整项目代码javapublic class B extends A {    int a = 100; // 与父类中的成员同名,且类型相同    char b = 'b'; // 与父类中的成员同名,但类型不同    public void methodB() {        System.out.println("methodB...");    }     public static void main(String[] args) {        B b = new B();        b.methodA();        b.methodB();//        b.methodC(); // 编译失败,在继承体系中没有发现 methodC()    }}一键获取完整项目代码java运行结果:当成员方法没有同名时,在子类方法中或通过子类对象访问方法时,若自己有,访问自己的,若自己没有,则在父类中找,若父类中也没有,则报错成员方法名相同public class B extends A {    int a = 100; // 与父类中的成员同名,且类型相同    char b = 'b'; // 与父类中的成员同名,但类型不同    public void methodA(int a) {        System.out.println("methodA..." + a);    }     public static void main(String[] args) {        B b = new B();        b.methodA();        b.methodA(10);    }}一键获取完整项目代码java 运行结果:当子类方法名和父类方法名相同,但其参数列表不同时,就构成了重载,根据调用方法时传递的参数选择合适的方法访问,若不存在该方法,则报错public class B extends A {    int a = 100; // 与父类中的成员同名,且类型相同    char b = 'b'; // 与父类中的成员同名,但类型不同     @Override    public void methodA() {        System.out.println("B methodA...");    }     public static void main(String[] args) {        B b = new B();        b.methodA();    }}一键获取完整项目代码java 当子类方法名和父类方法名相同,且其参数列表也相同时,就构成了重写,此时,会访问子类的方法那么,当子类中存在和父类相同的成员时,如何在子类中访问父类同名成员呢?super 关键字当子类和父类中存在相同名称的成员时,若要在子类方法中访问父类同名成员时,是不能直接访问的。Java 提供了 super 关键字,super 的主要作用:在子类方法中访问父类成员访问父类成员变量public class B extends A {    int a = 100; // 与父类中的成员同名,且类型相同    char b = 'b'; // 与父类中的成员同名,但类型不同     public void methodB() {        System.out.println(super.a);        System.out.println(super.b);    }     public static void main(String[] args) {        B b = new B();        b.methodB();    }}一键获取完整项目代码java访问父类成员方法public class B extends A {    int a = 100; // 与父类中的成员同名,且类型相同    char b = 'b'; // 与父类中的成员同名,但类型不同     public void methodB() {        super.methodA();        System.out.println("methodB...");    }     public static void main(String[] args) {        B b = new B();        b.methodB();    }}一键获取完整项目代码java需要注意的是:只能在非静态方法中使用 super访问父类构造方法在子类对象构造时,会先调用父类构造方法,然后再执行子类构造方法为 A 添加带一个参数的构造方法public class A {    int a = 10;    int b = 20;    public A(int a) {        System.out.println("a");    }    public void methodA() {        System.out.println("methodA...");    }}一键获取完整项目代码java 编译报错:A 中没有可用的默认构造函数为什么会报错呢?这是因为在 A 中没有定义的构造方法时,编译器提供了默认的无参构造方法子类 B 中也没有定义构造方法,编译器也提供了默认的无参构造方法,且在 B 的构造方法中第一行默认有隐含的 super() 调用父类的无参构造方法当添加了带有一个参数的构造方法后,编译器也就不再提供无参的构造方法,此时也就会报错我们可以在 A 中添加无参构造方法或是在 B 的无参构造方法中调用 A 带有一个参数的构造方法    public B(int a) {        super(a);    }一键获取完整项目代码java当父类中有多个构造方法时,就需要在子类构造方法中选择一个合适的父类构造方法调用,否则编译失败为什么要先调用父类的构造方法呢?子类对象中的成员是由两部分组成的:父类继承下来的成员 和 子类新增的成员 因此,在构造子类对象时,就需要先调用父类的构造方法,将从父类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增的成员初始化完整在子类构造方法中,通过 super(...) 调用父类构造,且 super(...) 必须是子类构造函数中的第一条语句因此,super(...) 只能在子类构造方法中出现一次,且必须在第一行而若需要在构造方法中使用 this 关键字调用类中的其他构造方法,也必须在第一行使用,也就是说,super(...) 和 this(...) 不能同时出现super 和 this相同点(1)都是 Java 中的关键字(2)都可以在成员方法中用来访问成员变量和调用其他成员方法,都可以作为构造方法的第一条语句(3)只能在类的非静态方法中使用,用来访问非静态成员方法和变量(4)在构造方法中调用时,必须是构造方法的第一条语句,并且不能同时存在不同点(1)this 表示当前对象(实例方法的对象)的引用,而 super 表示子类对象重父类继承下来部分成员的引用(2)在非静态成员方法中,this 用来访问本类的方法和属性,super 用来访问父类继承下来的方法和属性(3)在构造方法中,this(...) 用于调用本类构造方法,super(...) 用于调用父类构造方法,两种调用不能同时在构造方法中出现(4)构造方法中一定会存在 super(...) 的调用,但 this(...) 若没有写则没有初始化在前面的文章 Java 代码块-CSDN博客 中,我们学习了代码块,重点学习了 实例代码块 和 静态代码块,以及它们的执行顺序,接下来,我们就来看存在继承关系时,它们的执行顺序public class A {    int a;    int b;    {        a = 10;        b = 20;        System.out.println("A 构造代码块执行...");    }     static {        System.out.println("A 静态代码块执行...");    }    public A(int a) {        System.out.println("A 构造方法执行...");    }    public void methodA() {        System.out.println("methodA...");    }}一键获取完整项目代码javapublic class B extends A {    int a = 100; // 与父类中的成员同名,且类型相同    char b = 'b'; // 与父类中的成员同名,但类型不同     static {        System.out.println("B 静态代码块执行...");    }     {        System.out.println("B 构造代码块执行...");    }    public B(int a) {        super(a);        System.out.println("B 构造方法执行...");    }     public void methodA() {        System.out.println("methodB...");    }     public static void main(String[] args) {        B b1 = new B(10);        System.out.println("---------------------");        B b2 = new B(20);    }}一键获取完整项目代码java运行结果:   通过运行结果,可以看到:静态代码块先执行,并且只执行一次,在类加载阶段执行父类静态代码块优先于子类静态代码块执行,且最早执行当有对象创建时,才会执行实例代码块父类实例代码块和父类构造方法先执行,然后再执行子类的实例代码块和子类的构造方法访问限定符为了实现封装性,Java 引入了访问限定符,主要限定:类或类中成员能否在类外或其他包中被访问范围    private    default    protected    public同一个包中的同一个类    √    √    √    √同一个包中的不同类        √    √    √不同包中的子类            √    √不同包中的非子类                √public:在任何类都可以访问protected:在同一个包中的类或不同包中的子类可以访问default:同一个包中的类可以访问private:只有该类内部可以访问当 A 和 B 位于同一个包中时:public class A {    public int a;    int b;    protected int c;    private int d;}一键获取完整项目代码javapublic class B extends A {    public void method() {        super.a = 10;        super.b = 20;        super.c = 30;        super.d = 40;    }}一键获取完整项目代码java由于变量 d 是 A 私有的,即只能在 A 类中访问,因此,子类中不能直接访问但是,父类中的 private 成员变量虽然不能在子类中直接访问,但是也继承到子类中了子类可以通过父类提供的方法来进行修改public class A {    public int a;    int b;    protected int c;    private int d;     public int getD() {        return d;    }     public void setD(int d) {        this.d = d;    }}一键获取完整项目代码javapublic class B extends A {    public void method() {        super.a = 10;        super.b = 20;        super.c = 30;        super.setD(40);    }}一键获取完整项目代码java当 A 和 B 位于不同包中:public class B extends A {    public void method() {        super.a = 10; // 父类中 public 修饰的成员在不同包子类中可以直接访问        super.b = 20; // 父类中 protected 修饰的成员在不同包子类中可以直接访问        // super.c = 30; // 编译报错,父类中默认访问权限修饰的成员在不同包子类中不能直接访问        // super.d = 40; // 编译报错,父类中默认访问权限修饰的成员在不同包子类中不能直接访问    }}一键获取完整项目代码java继承方式在 Java 中,支持的继承方式有:(1)单继承(2)多层继承(3)不同类继承同一个类(4)多继承(不支持)Java 中不支持多继承类是对现实事物的抽象,而当情况比较复杂时,涉及到的类也会比较多,类之间的关系也会比较复杂但即使如此,我们并不希望类之间的继承层次太复杂,一般不希望出现超过三层的继承关系此时,若想从语法上限制继承,就可以使用 final 关键字final 关键字final 可以用来修饰变量、成员方法以及类当修饰变量时,表示该变量为常量(不可变)当修饰方法时,表示该方法不能被重写当修饰类时,表示该类不能被继承继承和组合与继承类似,组合也是一种表达类之间关系的方式,也能够达到代码重用的效果。但组合并没有涉及到特殊的语法,仅仅是将一个类的实例作为另一个类的字段继承表示的对象之间是 is a 的关系,如:猫 是 动物组合表示的对象之间是 has a 的关系,如:电脑 有 键盘组合:public class Person {    private Student[] students;}一键获取完整项目代码java此时 Student 属于 Person 的一部分,  在 Person 中,可以复用 Student 中的属性和方法继承: public class Student extends Person{ }一键获取完整项目代码javaStudent 是 Person 的子类,继承了父类的成员,能够复用父类中的属性和方法组合和继承都可以实现代码复用,是使用组合还是继承,需要根据具体的情况来进行选择————————————————原文链接:https://blog.csdn.net/2301_76161469/article/details/142621979
  • [技术干货] 【多线程】并发安全
    一、体会线程安全问题当我们编写一个多线程程序,要求两个线程对同一个变量(共享变量)进行修改,得到的结果是否与预期一致?创建两个线程,分别对共享变量(count)进行自增5万次操作,最后输出的结果理论上应为10万,但是实际上输出的结果是一个小于10万且不确定的数。读者可以自行实现一下该多线程程序,运行后看看结果是否符合预期。public class Demo14_threadSafety {    private static int count = 0;     public static void main1(String[] args) {        Thread t1 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                count++;            }            System.out.println("t1-结束");        });         Thread t2 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                count++;            }            System.out.println("t2-结束");        });         t1.start();        t2.start();         // 理论上输出的结果应是100000,实际输出的结果是0        // 原因是主线程 main 运行太快了,当 t1 和 t2 线程还在计算时,主线程已经打印结果、运行完毕了        System.out.println(count);    }     // 让主线程等待 t1 和 t2 线程,等到它们两个都执行完成再打印,故使用 join 方法    public static void main2(String[] args) throws InterruptedException {        Thread t1 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                count++;            }            System.out.println("t1-结束");        });         Thread t2 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                count++;            }            System.out.println("t2-结束");        });         t1.start();        t2.start();         // 在主线程中,通过 t1 和 t2 对象调用 join 方法        // 表示让主线程 main 等待 t1 线程和 t2 线程        t1.join();        t2.join();         // 当两个线程都执行完毕后主线程再继续执行打印操作        System.out.println(count);        // 实际输出的结果小于100000,仍不符合预期    }}一键获取完整项目代码java二、线程安全的概念通过上面的一个例子,想必读者已经体会到线程安全问题了吧?那究竟什么是线程安全问题呢?其原因是什么?如何解决线程安全问题呢?不要急,且听小编慢慢道来~如果在多线程环境下运行的程序其结果符合预期或与在单线程环境下运行的结果一致,就说这个程序是线程安全的,否则是线程不安全的。上面的例子在单线程环境下运行——比如来两个循环对共享变量进行自增操作,那么结果是符合预期的;但是在多线程环境下运行就不符合预期。因此该程序是线程不安全的,也可以说该程序存在线程安全问题。三、线程安全问题的原因究竟是哪里出问题导致程序出现线程安全问题呢?究其根本,罪魁祸首是 操作系统的线程调度有随机性/抢占式执行 。由于操作系统的线程调度是有随机性的,这就会存在这种情况:某一个线程还没执行完呢,就调度到其他线程去执行了,从而导致数据不正确。当然了,一个巴掌拍不响,还有以下三个导致线程不安全的原因:原子性:指 Java 语句,一条 Java 语句可能对应不止一条指令,若对应一条指令,就是原子的。可见性:一个线程对主内存(共享变量)的修改,可以及时被其他线程看到。有序性:一个线程观察其他线程中指令的执行顺序,由于 JVM 对指令进行了重排序,观察到的顺序一般比较杂乱。因其原理与 CPU 及编译器的底层原理有关,暂不讨论。之前的例子就是由于原子性没有得到保障而出现线程安全问题:public static void main2(String[] args) throws InterruptedException {    Thread t1 = new Thread(() -> {        for (int i = 0; i < 50000; i++) {            count++;        }        System.out.println("t1-结束");    });     Thread t2 = new Thread(() -> {        for (int i = 0; i < 50000; i++) {            count++;        }        System.out.println("t2-结束");    });     t1.start();    t2.start();     t1.join();    t2.join();     System.out.println(count);}一键获取完整项目代码java1. “count ++” 这条语句对应多条指令:读取数据、计算结果、存储数据。2. t1 线程和 t2 线程分别执行一次“count++”语句,期望的结果是“count = 2”,其过程如下:         初始情况:         当线程执行“count ++”时,总共分三个步骤:load、update、save,由于线程调度的随机性/抢占式执行,可能会出现以下情况(可能出现的情况有很多种,这里只是其中一种):        这时候 t1 正在执行“count ++”这条语句,执行了“load 和 update”指令后,t1 的工作内存(寄存器)存着更新后的值,但是还未被写回内存中:        接着调度到 t2 线程并开始执行“count ++”语句,并且语句中包含的三条指令都执行。此时由于 t1 更新后的 count 的值还未写回内存,因此 t2 执行 load 操作所获取到的 count 仍是 0。接着 t2 执行 update 和 save 指令:        当 t2 执行完成,内存的 count 已被修改为 1 。此时调度回 t1 线程并继续执行 save 指令,但是 t1 线程寄存器中 count 的值也是 1 ,此时写回内存更新后 count 的值依然是 1 。         结果 count = 1,与预期的 count = 2 不符,因此存在线程安全问题,其原因是操作系统的随机线程调度和 count 语句存在非原子性。四、解决线程安全问题的方法从上面的例子我们知道,当一条语句的指令被拆开来执行的话是存在线程安全问题的,但是,当我们将“count ++”这条语句的三个指令都放在一起执行怎么样?         当线程调度的情况如下:        此时 t1 线程开始执行“count ++”语句的 load、update 和 save 指令。内存中的  count 为 0,t1 读取到内存中的 count 之后更新至 1 并写回内存中。当 t1 执行完成后内存的 count 由 0 更新至 1:        接着调度至 t2 线程,开始执行“count ++”语句的 load、update 和 save 指令。经过更新后内存中的 count 为 1,此时 t2 读取 count 并更新为 2,然后写回内存中。当 t2 执行完成,内存中的 count 就更新成 2 了:可以发现,结果与预期相符!说明这个方法可行。可以将操作顺序改成先让 t1 线程完成“count ++”操作,再让 t2 线程完成该操作——即串行执行。现在我们对之前的例子进行优化:// 可以试着让 t1 线程先执行完后,再让 t2 线程执行,改成串行执行public static void main3(String[] args) throws InterruptedException {    Thread t1 = new Thread(() -> {        for (int i = 0; i < 50000; i++) {            count++;        }        System.out.println("t1-结束");    });     Thread t2 = new Thread(() -> {        for (int i = 0; i < 50000; i++) {            count++;        }        System.out.println("t2-结束");    });     t1.start();    t1.join();     t2.start();    t2.join();     System.out.println(count);}一键获取完整项目代码java刚刚是让一个线程一次性执行“count ++”这条语句的三个指令,也就是说,我们是通过这样操作将原本是非原子的三条指令打包成了一个原子指令(即执行过程中不可被打断——调度走)。这样就有效的解决了线程安全问题。而上述的操作,其实就是 Java 中的 加锁操作 。当一个线程执行一个非原子的语句时,通过加锁操作可以防止在执行过程中被调度走或被其他线程打断,若其他线程想要执行该语句,则要进入 阻塞等待 的状态,当线程执行完毕并将锁释放,操作系统这时唤醒等待中的线程,才可以执行该语句。就相当于上厕所:当厕所内没有人时(没有线程加锁),就可以使用;当厕所内有人时(已经有线程加锁了),那么就必须等里面的人出来后才能使用。注意:前一个线程解锁之后,并不是后一个线程立刻获取到锁。而是需要靠操作系统唤醒阻塞等待中的线程的。若 t1、t2 和 t3 三个线程竞争同一个锁,当 t1 线程获取到锁,t2 线程再尝试获取锁,接着 t3 线程尝试获取锁,此时 t2 和 t3 线程都因获取锁失败而处于阻塞等待状态。当 t1 线程释放锁之后,t2 线程并不会因为先进入阻塞状态在被唤醒后比 t3 先拿到锁,而是和 t3 进行公平竞争。(不遵循先来后到原则)4.1 synchronized 关键字在处理由原子性导致的线程安全问题时,通常采用加锁操作。加锁 / 解锁这些操作本身是在操作系统所提供的 API 中的,很多编程语言对其进行了封装,Java 中使用 synchronized 关键字来进行加锁 / 解锁操作,其底层是使用操作系统的 mutex lock 来实现的。Java 中的任何一个对象都可以用作“锁”。synchronized (锁对象){             ——> 进入代码块,相当于加锁操作        // 一些需要保护的逻辑}                                                       ——> 出了代码块,相当于解锁操作当多个线程针对同一个锁对象竞争的时候,加锁操作才有意义。对之前的例子进行加锁操作:public class Demo15_synchronized {    private static int count = 0;     public static void main1(String[] args) throws InterruptedException {        Object locker = new Object();         Thread t1 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                synchronized (locker) {                    count++;                }            }            System.out.println("t1-结束");        });         Thread t2 = new Thread(() -> {            for (int i = 0; i < 50000; i++) {                synchronized (locker) {                    count++;                }            }            System.out.println("t2-结束");        });         t1.start();        t2.start();         t1.join();        t2.join();         System.out.println(count);    }}一键获取完整项目代码javasynchronized 关键字用来修饰 普通方法 时,相当于给 this 加锁;synchronized 关键字用来修饰 静态方法 时,相当于给 类对象 加锁。于是可以使用另一种写法:// 写法二:// 将 count++ 所包含的三个操作封装成一个 add 方法// 使用 synchronized 修饰 add 方法 class Counter {    private int count = 0;     synchronized public void add () {        // synchronized 修饰普通方法相当于给 this 加锁        count++;    }    // 相当于:    // public void add () {    //     synchronized (this) {    //         count++;    //     }    // }     public int get () {        return count;    }} public static void main(String[] args) throws InterruptedException {    Counter counter = new Counter();     Thread t1 = new Thread(() -> {        for (int i = 0; i < 50000; i++) {            counter.add();        }    });     Thread t2 = new Thread(() -> {        for (int i = 0; i < 50000; i++) {            counter.add();        }    });     t1.start();    t2.start();     t1.join();    t2.join();     System.out.println(counter.get());}一键获取完整项目代码java这样一来,就成功解决了多线程程序中由原子性导致的线程安全问题。4.2 volatile 关键字我们再来一个例子:让 t1 线程读取共享变量的值,然后让 t2 线程修改共享变量的值,我们可以写出如下程序:public class Demo17_volatile {    private static int flag = 0;     public static void main1(String[] args) {        Thread t1 = new Thread(() -> {            while (flag == 0) {                // 当 flag 为 0 时一直循环            }            System.out.println("t1-结束");        });         Thread t2 = new Thread(() -> {            // 对 flag 进行修改            Scanner in = new Scanner(System.in);            System.out.println("请输入 flag 的值:");            flag = in.nextInt();        });         t1.start();        t2.start();        // 运行后发现即使输入1,t1 线程并不会结束    }}一键获取完整项目代码java我们期望该程序运行后,输入非零的数如 1,t1 线程能够结束,实际并非如此,很显然,这也是出现了线程安全问题。这次出现问题的原因并非原子性,而是 可见性 。我们说过,可见性是一个线程对主内存(共享变量)的修改能够被其他线程及时看到,若不能被其他线程及时看到,就会出现数据错误,从而导致线程安全问题。如果我们使用锁来处理的话,好像并不能解决。我们先来认识一个东西:编译器优化。由于不能保证写代码的人每一次写代码都不会出错,所以开发 JDK 的先人们就让编译器 / JVM 能够根据代码的原有逻辑进行优化。在我们这个程序中:while {...load...cmp...} 先将数据从内存中 load 到寄存器中,然后在寄存器进行 “条件比较” 指令 cmp 。在短时间内用户还没来得及输入呢,但是这个循环语句能够执行成万上亿次且内存中的 flag 的值一直都是 0。那么这时候编译器 / JVM 就察觉到:既然一直读取 flag 都是同一个值,那干脆我直接读取寄存器算了(读取寄存器开销更小)。这样一来,当用户输入 1 的时候(存入内存),t1 线程就无法读取通过 t2 线程更新之后的值了,也就不会结束。在上面的程序中,t2 线程修改后的值无法被 t1 线程看到,这就是可见性导致的线程安全问题,由于编译器我们没办法更改,所以只好另寻他法。如果我们能让循环执行的慢一点,是不是就能解决问题了?尝试优化该程序:public static void main2(String[] args) {    Thread t1 = new Thread(() -> {        while (flag1 == 0) {            // 当 flag1 为 0 时一直循环            try {                Thread.sleep(1);                // 放慢读取速度            } catch (InterruptedException e) {                throw new RuntimeException(e);            }        }        System.out.println("t1-结束");    });     Thread t2 = new Thread(() -> {        // 对 flag1 进行修改        Scanner in = new Scanner(System.in);        System.out.println("请输入 flag1 的值:");        flag1 = in.nextInt();    });     t1.start();    t2.start();}一键获取完整项目代码java在运行优化版的程序后,发现问题被解决了,其原因是:使用了 sleep(1)—— 表示每一次读取的时候休眠 1毫秒,这对于读取操作的速度来说已经很慢了。因此不会触发编译器优化,也就不会出现可见性导致的线程安全问题了。但是,在实际开发环境中频繁使用 sleep 的话会导致程序效率下降,这样的话用户体验就会变差。Java 中使用 volatile 关键字来处理 可见性 导致的线程安全问题。使用 volatile 关键字来修饰共享变量,这样一来这个变量无论如何都不会被编译器优化。具体流程是:当 t1 线程读取被修饰的变量时,会强制读取主内存中变量最新的值;当 t2 线程修改被修饰的变量时,在工作内存(寄存器)中更新变量的值之后,立即将改变的值刷新至主内存。加上 volatile 关键字强制读取内存,虽然速度慢了,但是保证数据不出错。使用 volatile 关键字优化后的程序:// 使用 volatile 关键字来修饰被读取的变量,此时无论读取速度怎样,JVM 都不会对该变量进行优化private volatile static int flag = 0; public static void main(String[] args) {    Thread t1 = new Thread(() -> {        while (flag == 0) {            // 当 flag 为 0 时一直循环        }        System.out.println("t1-结束");    });     Thread t2 = new Thread(() -> {        // 对 flag 进行修改        Scanner in = new Scanner(System.in);        System.out.println("请输入 flag 的值:");        flag = in.nextInt();    });     t1.start();    t2.start();}一键获取完整项目代码java注意:volatile 关键字只能保证可见性,不能保证原子性,因此不能完全解决线程安全问题。当多线程中存在 ++、--、+=、-=、*= 和 /= 或者类似的操作符时就不能够使用 volatile 关键字来处理,需要使用 synchronized 关键字。五、死锁那么话又说回来,如果对锁使用不当,就很容易出现死锁。如果一个线程已经获取了锁,再次获取这个锁,能成功吗?会出现死锁吗?Object locker = new Object(); synchronized (locker) {    synchronized (locker) {        // 一些需要保护的逻辑    }}一键获取完整项目代码java按照前面的逻辑,第一次获取锁对象后,该锁对象已被占有,下一次获取该锁对象时应该会触发阻塞等待。而要想解除阻塞等待就得往下继续执行,但是要想往下执行就得将锁解开。这样不就构成死锁了嘛?不妨试试在你的 IDE 上运行一下~5.1 synchronized 的可重入性如果你在 IDE 上运行了刚刚的程序,可以发现程序是没问题的。按照我们这之前的逻辑,当锁对象第一次被获取到,其他的线程再想获取锁对象时就只能阻塞等待。但是,第二次获取锁对象的是同一个线程呀~ 因此并不会触发阻塞等待。Java 中的 synchronized 关键字是具有可重入性的,即对于同一个线程可以重复获取同一个锁对象。可重入锁内部包含了“线程持有者”和“计数器”两个信息,若线程加锁时发现锁已被占有且恰好是自己,此时仍可以获取到锁,让计数器自增即可;当计数器为 0 时,将锁释放,其他线程可以才获取到锁。这样就可以避免死锁的出现——开发 JDK 的前人们真的为我们考虑了太多😭5.2 死锁的概念当有两个线程为了保护两个不同的共享资源而使用两个不同的锁且这两个锁使用不当时,就会造成两个线程都在等待对方解锁,在没有外界干扰的情况下他们会一直相互等待,此时就是发生了死锁。死锁需要同时具备以下四个条件:互斥条件:要求同一个资源不能被多个线程同时占有。不可剥夺条件:当资源已被某个线程占有,其他线程只能等到该线程使用完并释放后才能获取,不可以强行打断并获取。持有并等待条件:有三个线程两个资源,当线程 1 已经占有资源 A(线程 2 尝试获取资源 A 但触发阻塞等待)且尝试获取资源 B 时(此时资源 B 已被线程 3 占有),线程 1 就会触发阻塞等待且不释放手中持有的资源 A 。循环等待/环路等待条件:有两个线程两个资源,线程 1 已占有资源 A 并想要获取资源 B ,但是资源 B 已被线程 2 占有,并且线程 2 在占有资源 B 的同时想要获取资源 A 。比如,有两个线程 t1 和 t2 以及两个锁 locker1 和 locker2,t1 先获取 locker1 并尝试获取 locker2;t2 先获取 locker2 并尝试获取 locker1:// 两个线程两把锁,每个线程获取到一把锁之后尝试获取对方的锁public static void main2(String[] args) throws InterruptedException {    Object locker1 = new Object();    Object locker2 = new Object();     Thread t1 = new Thread(() -> {        synchronized (locker1) {            // t1 线程获取到锁对象 locker1            try {                Thread.sleep(1000);                // 确保 t2 线程拿到 locker2            } catch (InterruptedException e) {                throw new RuntimeException(e);            }            // 尝试获取锁对象 locker2            synchronized (locker2) {                System.out.println("t1 线程获取到两把锁");            }        }    });     Thread t2 = new Thread(() -> {        synchronized (locker2) {            // t2 线程获取到锁对象 locker2            try {                Thread.sleep(1000);                // 确保 t1 线程拿到 locker1            } catch (InterruptedException e) {                throw new RuntimeException(e);            }            // 尝试获取锁对象 locker1            synchronized (locker1) {                System.out.println("t2 线程获取到两把锁");            }        }    });     t1.start();    t2.start();     t1.join();    t2.join();}一键获取完整项目代码java运行该程序会发现什么也没输出,程序却还在运行。这就是发生了死锁。我们借助第三方工具来观察线程的状态:5.3 如何避免死锁要想避免死锁,我们就要从死锁的四个条件入手,其中,互斥条件和不可剥夺条件基本上是无法打破的,因为这两个是 synchronized 锁的基本特性,因此我们选择从后两个条件入手。1. 持有并等待条件:通常是由于代码中的嵌套加锁导致的,因此选择将嵌套的加锁代码改成串行的加锁代码(先将已占有的资源释放掉,然后去获取另一个资源):// 避免死锁的写法:打破持有并等待条件——将嵌套加锁改成串行加锁public static void main(String[] args) throws InterruptedException {    Object locker1 = new Object();    Object locker2 = new Object();     Thread t1 = new Thread(() -> {        synchronized (locker1) {            // t1 线程获取到锁对象 locker1            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                throw new RuntimeException(e);            }        }        // t1 线程释放已占有的锁对象 locker1        // 尝试获取锁对象 locker2        synchronized (locker2) {            System.out.println("t1 线程获取到两把锁");            // t1 获取到两把锁之后结束执行,此时 locker1 和 locker2 均被释放        }    });     Thread t2 = new Thread(() -> {        synchronized (locker2) {            // t2 线程获取到锁对象 locker2            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                throw new RuntimeException(e);            }        }        // t2 线程释放已占有的锁对象 locker2        // 尝试获取锁对象 locker1        synchronized (locker1) {            System.out.println("t2 线程获取到两把锁");            // t2 获取到两把锁后结束执行        }    });     t1.start();    t2.start();     t1.join();    t2.join();}一键获取完整项目代码java2. 循环等待/环路等待条件:这种情况一般都是双方都持有对方想要的锁但是都不肯释放,所以我们就调整一下顺序:让 t1 和 t2 获取锁的顺序都是 locker1 和 locker2,当 t1 已占有 locker1 时 t2 想获取只能阻塞等待,但这时 t1 可以获取 locker2,等待 t1 两把锁都获取到并释放之后,t2 被唤醒并且获取两把锁。// 防止死锁的写法:打破循环等待条件——调整加锁顺序public static void main(String[] args) throws InterruptedException {    Object locker1 = new Object();    Object locker2 = new Object();     Thread t1 = new Thread(() -> {        synchronized (locker1) {            // t1 线程获取到锁对象 locker1            try {                Thread.sleep(1000);                // 此时 t2 因 locker1 被 t1 获取而处于阻塞状态 BLOCKED                // t1 休眠结束后,获取 locker2            } catch (InterruptedException e) {                throw new RuntimeException(e);            }            // 尝试获取锁对象 locker2            synchronized (locker2) {                System.out.println("t1 线程获取到两把锁");                // t1 获取到两把锁之后结束执行,此时 locker1 和 locker2 均被释放            }        }    });     Thread t2 = new Thread(() -> {        synchronized (locker1) {            // t2 线程获取到锁对象 locker1            // 因 t1 先获取到 locker1 ,t2 此时处于阻塞状态 BLOCKED            // 当 t1 结束执行后,t2 恢复执行并获取 locker1            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                // 休眠结束后继续获取 locker2                throw new RuntimeException(e);            }            // 尝试获取锁对象 locker2            synchronized (locker2) {                System.out.println("t2 线程获取到两把锁");                // t2 获取到两把锁后结束执行            }        }    });     t1.start();    t2.start();     t1.join();    t2.join();}一键获取完整项目代码java今天暂且到这吧~————————————————原文链接:https://blog.csdn.net/REGARD712/article/details/155878355
  • [技术干货] 【全网最细】CentOS 安装 JDK 1.8 实操指南(避坑版)
    一、下载 JDK 1.8 安装包JDK 1.8 是企业级应用的经典稳定版本,优先从官方渠道下载适配 Linux 64 位的压缩包:官方下载地址:Java Downloads | Oracle小技巧:Oracle 官网下载需登录,若嫌麻烦,可选择华为云 / 阿里云镜像站(如 https://mirrors.huaweicloud.com/openjdk),下载速度更快且无需登录。 二、清理系统自带 JDK(关键避坑步骤)CentOS 系统默认可能预装 OpenJDK,与 Oracle JDK 冲突,需彻底清理:# 1. 检查已安装的 Java 相关包(忽略大小写,避免漏查)rpm -qa | grep -i java # 2. 强制卸载所有 Java 包(无依赖检查,彻底清理)rpm -qa | grep -i java | xargs -n1 rpm -e --nodeps一键获取完整项目代码bash命令解析:rpm -qa:列出系统所有已安装的 RPM 包;grep -i:忽略大小写匹配 java/Java/JAVA;xargs -n1:逐个传递包名,避免批量卸载报错;rpm -e --nodeps:强制卸载,忽略包间依赖(清理更彻底)。三、安装 JDK 1.8(规范目录 + 高效操作)Linux 软件建议统一安装在 /usr/local 目录,便于管理:# 1. 将下载的压缩包移动到 /usr/local(替换为实际文件路径)sudo mv ~/jdk-8u421-linux-x64.tar.gz /usr/local/ # 2. 进入安装目录cd /usr/local # 3. 解压压缩包(xf 组合:x=解压,f=指定文件,无需加 v 减少冗余输出)sudo tar xf jdk-8u421-linux-x64.tar.gz # 4. 删除压缩包(节省磁盘空间,可选)sudo rm -f jdk-8u421-linux-x64.tar.gz # 5. 重命名目录(可选,简化后续配置,避免版本号过长)sudo mv jdk1.8.0_421 jdk1.8一键获取完整项目代码bash注意:解压后目录名默认是 jdk1.8.0_421(与压缩包版本对应),重命名为 jdk1.8 可避免后续环境变量因版本号变更出错。四、配置全局环境变量(永久生效)环境变量写入 /etc/profile 可对所有用户生效,避免仅当前用户可用:# 1. 编辑 profile 文件(新手推荐用 nano,比 vi 更易操作)sudo vi /etc/profile# 或 sudo nano /etc/profile # 2. 在文件末尾添加以下配置(复制粘贴即可,注意 JAVA_HOME 路径)export JAVA_HOME=/usr/local/jdk1.8export JRE_HOME=${JAVA_HOME}/jreexport CLASSPATH=.:${JAVA_HOME}/lib/dt.jar:${JAVA_HOME}/lib/tools.jar:${JRE_HOME}/libexport PATH=${JAVA_HOME}/bin:${JRE_HOME}/bin:$PATH一键获取完整项目代码bash配置解析:JAVA_HOME:JDK 根目录(核心配置,必须与实际安装路径一致);CLASSPATH:补充 dt.jar 和 tools.jar,解决部分项目类加载失败问题;PATH:将 JDK/bin 加入系统路径,使 java/javac 命令全局可用。# 3. 刷新配置文件(立即生效,无需重启服务器)source /etc/profile # 4. 验证环境变量(可选,确认配置无误)echo $JAVA_HOME# 输出 /usr/local/jdk1.8 则说明配置正确一键获取完整项目代码bash五、验证安装结果(一步确认)执行以下命令,若输出如下版本信息,说明安装成功:java -version一键获取完整项目代码bash成功输出示例:java version "1.8.0_421"Java(TM) SE Runtime Environment (build 1.8.0_421-b09)Java HotSpot(TM) 64-Bit Server VM (build 25.421-b09, mixed mode)一键获取完整项目代码bash六、常见问题速解(99% 的人会踩的坑)问题现象    解决方法执行 java -version 提示 “命令未找到”    1. 检查 JAVA_HOME 路径是否与实际解压目录一致;2. 重新执行 source /etc/profile,或退出当前终端重新登录;3. 确认命令以 root/sudo 权限执行。解压后无 jre 目录    部分 JDK 1.8 版本需手动生成 JRE:cd $JAVA_HOME && ./bin/jlink --module-path jmods --add-modules java.desktop --output jre环境变量配置后仅 root 用户可用    避免将配置写入 ~/.bash_profile,必须写入 /etc/profile;执行 chmod 644 /etc/profile 确保普通用户可读取。解压时报 “权限不足”    执行 sudo chmod 755 jdk-8u421-linux-x64.tar.gz 赋予文件执行权限。七、进阶优化(生产环境推荐)配置软链接:避免后续升级 JDK 需修改环境变量sudo ln -s /usr/local/jdk1.8 /usr/local/java# 环境变量中 JAVA_HOME 改为 /usr/local/java 即可一键获取完整项目代码bash检查依赖库:部分系统缺少 libc 库导致 JDK 启动失败sudo yum install -y glibc.i686 glibc.x86_64一键获取完整项目代码bash总结JDK 1.8 安装核心是 “清理旧版本 + 路径准确 + 全局配置”,按本教程操作可避开 90% 的常见问题。安装完成后,可直接部署 Tomcat、Spring Boot 等 Java 项目,适配绝大多数企业级应用场景。————————————————原文链接:https://blog.csdn.net/m0_73483832/article/details/156015201
  • [技术干货] 新手向: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
总条数:696 到第
上滑加载中