• [技术干货] Java中锁的全面解析之类型、使用场景、优缺点及实现方式(示例代码【转载】
    Java中锁的全面解析:类型、使用场景、优缺点及实现方式在多线程编程中,锁是保证数据一致性和线程安全的核心机制。Java 提供了丰富的锁机制来应对不同的并发场景。本文将从锁的基本概念出发,详细讲解 Java 中常见的锁类型、它们的使用场景、优缺点以及底层实现原理,并通过代码示例帮助读者深入理解。1. 锁的基本概念锁是一种同步机制,用于控制多个线程对共享资源的访问。当一个线程获取锁后,其他线程必须等待该锁被释放才能继续执行,从而避免竞态条件(Race Condition)。2. Java 中常见的锁类型2.1 互斥锁(Mutex Lock)特点:一次只允许一个线程持有锁。保证临界区的独占访问。常见实现:synchronized 关键字(内置锁)ReentrantLock(可重入锁)代码示例:使用 synchronized12345678910111213141516public class Counter {    private int count = 0;    // 同步方法,使用对象锁(this)    public synchronized void increment() {        count++;    }    // 同步代码块,使用指定对象锁    public void decrement() {        synchronized (this) {            count--;        }    }    public int getCount() {        return count;    }}优点:简单易用,无需手动释放锁。JVM 自动管理锁的获取与释放。缺点:无法中断等待中的线程。不能设置超时时间。只能是非公平锁(默认)。2.2 可重入锁(Reentrant Lock)特点:支持同一个线程多次获取同一把锁(即“可重入”)。提供更灵活的控制能力。代码示例:使用 ReentrantLock12345678910111213141516171819202122232425import java.util.concurrent.locks.ReentrantLock;public class ReentrantExample {    private final ReentrantLock lock = new ReentrantLock();    private int count = 0;    public void increment() {        lock.lock();        try {            count++;            // 可以再次获取锁(可重入)            if (count == 1) {                System.out.println("Thread " + Thread.currentThread().getName() + " is re-entering the lock.");            }        } finally {            lock.unlock();        }    }    public int getCount() {        lock.lock();        try {            return count;        } finally {            lock.unlock();        }    }}优点:支持可中断的锁获取(lockInterruptibly())。支持超时锁获取(tryLock(timeout))。支持公平锁和非公平锁(通过构造函数选择)。缺点:必须手动释放锁,容易忘记 unlock() 导致死锁。语法比 synchronized 复杂。2.3 读写锁(ReadWriteLock)特点:分离读操作和写操作的锁。多个读线程可以同时访问共享资源,但写操作必须独占。常见实现:ReentrantReadWriteLock代码示例:读写锁的应用1234567891011121314151617181920212223242526import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteExample {    private final ReadWriteLock lock = new ReentrantReadWriteLock();    private String data = "Default";    // 读操作:多个线程可同时读    public String readData() {        lock.readLock().lock();        try {            System.out.println(Thread.currentThread().getName() + " is reading data: " + data);            return data;        } finally {            lock.readLock().unlock();        }    }    // 写操作:独占访问,其他读写均被阻塞    public void writeData(String newData) {        lock.writeLock().lock();        try {            System.out.println(Thread.currentThread().getName() + " is writing data: " + newData);            data = newData;        } finally {            lock.writeLock().unlock();        }    }}优点:读多写少的场景下性能显著提升(减少锁竞争)。提高并发效率。缺点:写操作会阻塞所有读操作,可能导致“写饥饿”(Writer Starvation)。逻辑复杂度增加。2.4 原子锁(Atomic Lock)特点:使用原子操作(CAS)实现无锁编程。避免传统锁带来的上下文切换开销。常见类:AtomicInteger,AtomicReference,StampedLock(分段锁)代码示例:使用 AtomicInteger12345678910import java.util.concurrent.atomic.AtomicInteger;public class AtomicCounter {    private final AtomicInteger count = new AtomicInteger(0);    public void increment() {        count.incrementAndGet(); // CAS 操作,原子递增    }    public int getCount() {        return count.get();    }}优点:无锁机制,避免线程阻塞。性能高,适合高并发场景。缺点:无法处理复杂的复合操作(如“读-修改-写”)。存在 ABA 问题(可通过 AtomicStampedReference 解决)。2.5 StampedLock(戳记锁)特点:JDK 8 引入,支持乐观读锁(Optimistic Read)。读写分离,支持读写锁和乐观读锁三种模式。代码示例:StampedLock 用法12345678910111213141516171819202122232425262728293031import java.util.concurrent.locks.StampedLock;public class StampedLockExample {    private final StampedLock stampedLock = new StampedLock();    private double x, y;    // 乐观读:不加锁,先尝试读取,失败则升级为悲观读    public double distanceFromOrigin() {        long stamp = stampedLock.tryOptimisticRead();        double currentX = x, currentY = y;        // 检查是否在读期间发生了写操作(版本变化)        if (!stampedLock.validate(stamp)) {            stamp = stampedLock.readLock();            try {                currentX = x;                currentY = y;            } finally {                stampedLock.unlockRead(stamp);            }        }        return Math.sqrt(currentX * currentX + currentY * currentY);    }    // 写操作:独占锁    public void move(double deltaX, double deltaY) {        long stamp = stampedLock.writeLock();        try {            x += deltaX;            y += deltaY;        } finally {            stampedLock.unlockWrite(stamp);        }    }}优点:读操作性能极高(乐观读无需阻塞)。适用于读多写少且对读性能要求极高的场景。缺点:API 复杂,需要显式验证版本。容易出错(如忘记验证或释放锁)。3. 锁的使用场景总结锁类型适用场景推荐程度synchronized简单同步,小范围临界区⭐⭐⭐⭐⭐ReentrantLock需要超时、中断、公平性控制⭐⭐⭐⭐☆ReentrantReadWriteLock读多写少的共享数据⭐⭐⭐⭐☆AtomicXXX简单计数器、状态标志⭐⭐⭐⭐⭐StampedLock极高读性能需求,读多写少⭐⭐⭐☆☆4. 锁的底层实现原理(简述)synchronized:基于 JVM 的对象头(Mark Word)实现,通过 Monitor 机制管理锁状态。ReentrantLock:基于 AQS(AbstractQueuedSynchronizer)实现,使用 CAS+FIFO 队列管理线程排队。StampedLock:基于版本戳(Stamp)和状态位管理,支持乐观读。5. 最佳实践建议优先使用 synchronized,除非有特殊需求。避免在 finally 块外调用 unlock(),防止死锁。读写锁适用于读多写少的场景,避免“写饥饿”。原子类适合简单操作,复杂逻辑仍需锁保护。StampedLock 适合高性能读场景,但需谨慎使用。
  • [技术干货] Java中锁分类及在什么场景下使用【转载】
    一、基础分类(按实现方式)这是最核心的分类维度,直接决定锁的使用方式和核心能力。1. 内置锁(synchronized)- 隐式锁核心定义Java 关键字,JVM 层面实现的隐式锁(无需手动释放),是最基础、使用最广泛的锁。JDK1.6 后引入「锁升级」机制,性能大幅提升。核心特点可重入、默认非公平锁;自动加锁 / 解锁(方法 / 代码块执行完自动释放,无需手动处理);底层依赖对象头的Mark Word + 监视器锁(ObjectMonitor);支持锁升级(偏向锁→轻量级锁→重量级锁),适配不同并发场景。适用场景简单互斥场景(如方法 / 代码块的线程安全);并发度不高、代码简洁性优先的场景;不需要灵活特性(如可中断、超时获取锁)的场景;绝大多数普通业务场景(JVM 优化后性能接近显式锁)。代码示例1234567891011121314151617181920212223242526public class SynchronizedDemo {    // 1. 实例方法锁(对象锁):锁当前实例对象    public synchronized void objectLock() {        System.out.println(Thread.currentThread().getName() + "获取对象锁");        try { Thread.sleep(100); } catch (InterruptedException e) {}    }    // 2. 静态方法锁(类锁):锁当前类的Class对象    public static synchronized void classLock() {        System.out.println(Thread.currentThread().getName() + "获取类锁");        try { Thread.sleep(100); } catch (InterruptedException e) {}    }    // 3. 代码块锁:自定义锁对象(灵活度最高)    private final Object lockObj = new Object();    public void blockLock() {        synchronized (lockObj) {            System.out.println(Thread.currentThread().getName() + "获取代码块锁");            try { Thread.sleep(100); } catch (InterruptedException e) {}        }    }    public static void main(String[] args) {        SynchronizedDemo demo = new SynchronizedDemo();        // 竞争同一对象锁,串行执行        new Thread(demo::objectLock, "线程1").start();        new Thread(demo::objectLock, "线程2").start();    }}2. 显式锁(Lock 接口)- 手动锁核心定义JUC 包下java.util.concurrent.locks.Lock接口的实现类,手动加锁 / 释放锁(需在finally中释放,避免死锁),是 synchronized 的补充和增强。核心实现类 & 特点实现类核心特性ReentrantLock可重入、支持公平 / 非公平、可中断、超时获取锁ReentrantReadWriteLock读写分离(读共享、写独占)、可重入StampedLock支持乐观读、读写锁、写锁,性能优于读写锁适用场景需要灵活锁控制(如可中断、超时获取锁、公平锁)的场景;读多写少的高并发场景(选 ReentrantReadWriteLock/StampedLock);高并发、需要精细控制锁生命周期的场景。代码示例(ReentrantLock)1234567891011121314151617181920212223242526import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockDemo {    // 公平锁(按请求顺序获取),默认非公平锁(性能更高)    private static final ReentrantLock lock = new ReentrantLock(true);    public static void doTask() {        // 1. 加锁(可替换为lockInterruptibly():可中断锁)        lock.lock();        try {            System.out.println(Thread.currentThread().getName() + "获取锁");            // 模拟业务操作            Thread.sleep(100);        } catch (InterruptedException e) {            Thread.currentThread().interrupt(); // 恢复中断状态        } finally {            // 2. 释放锁(必须放finally,否则死锁)            if (lock.isHeldByCurrentThread()) {                lock.unlock();                System.out.println(Thread.currentThread().getName() + "释放锁");            }        }    }    public static void main(String[] args) {        new Thread(ReentrantLockDemo::doTask, "线程A").start();        new Thread(ReentrantLockDemo::doTask, "线程B").start();    }}二、进阶分类(按锁的核心特性)基于锁的行为和并发特性分类,帮你理解锁的底层逻辑和适用场景。1. 可重入锁 vs 不可重入锁类型定义示例适用场景可重入锁同一线程可多次获取同一把锁,不会死锁synchronized、ReentrantLock所有业务场景(递归调用、同一线程多次操作共享资源)不可重入锁同一线程多次获取同一锁会死锁自定义简单自旋锁(未处理重入)极少使用(仅严格限制锁获取次数的特殊场景)可重入锁示例(synchronized 递归调用)123456789101112public class ReentrantDemo {    public synchronized void outer() {        System.out.println("外层方法获取锁");        inner(); // 同一线程再次获取同一锁,无死锁    }    public synchronized void inner() {        System.out.println("内层方法获取锁");    }    public static void main(String[] args) {        new ReentrantDemo().outer(); // 正常执行,无死锁    }}2. 乐观锁 vs 悲观锁这是并发设计思想的分类,而非具体锁实现。类型核心思想实现方式适用场景悲观锁假设必有竞争,先锁后执行synchronized、ReentrantLock高冲突、写多读少(如库存扣减、转账)乐观锁假设无竞争,先执行后检测CAS(Atomic 类)、版本号低冲突、读多写少(如计数器、缓存更新)乐观锁示例(CAS 实现)12345678910111213141516171819import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockDemo {    // CAS是乐观锁的核心实现    private static final AtomicInteger count = new AtomicInteger(0);    // 原子自增(无锁,冲突时重试)    public static void increment() {        count.incrementAndGet();    }    public static void main(String[] args) throws InterruptedException {        Runnable task = () -> {            for (int i = 0; i < 1000; i++) increment();        };        Thread t1 = new Thread(task);        Thread t2 = new Thread(task);        t1.start(); t2.start();        t1.join(); t2.join();        System.out.println("最终计数:" + count.get()); // 2000(线程安全)    }}3. 公平锁 vs 非公平锁类型定义示例适用场景公平锁按请求顺序获取锁,先到先得ReentrantLock(true)对公平性要求高(如任务排队、避免线程饥饿)非公平锁不按顺序,线程可插队获取锁synchronized、ReentrantLock()大部分场景(优先性能,容忍轻微饥饿)4. 读写锁(ReentrantReadWriteLock)- 共享 + 独占锁核心定义将锁拆分为「读锁(共享锁)」和「写锁(独占锁)」,核心规则:读 - 读共享:多个线程可同时获取读锁;读 - 写互斥:读锁和写锁不能同时持有;写 - 写互斥:多个线程不能同时获取写锁。适用场景读多写少的场景(如缓存、配置读取、商品详情页、数据查询),相比普通独占锁,能大幅提升读并发效率。代码示例12345678910111213141516171819202122232425262728293031323334353637383940import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockDemo {    private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();    private static final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();    private static final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();    private static int cacheData = 0; // 模拟缓存数据    // 读操作(共享锁,多线程同时执行)    public static void readCache() {        readLock.lock();        try {            System.out.println(Thread.currentThread().getName() + "读取缓存:" + cacheData);            Thread.sleep(200); // 模拟读耗时        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        } finally {            readLock.unlock();        }    }    // 写操作(独占锁,串行执行)    public static void updateCache(int newData) {        writeLock.lock();        try {            System.out.println(Thread.currentThread().getName() + "更新缓存:" + newData);            cacheData = newData;            Thread.sleep(200); // 模拟写耗时        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        } finally {            writeLock.unlock();        }    }    public static void main(String[] args) {        // 5个读线程(可同时执行,并发效率高)        for (int i = 0; i < 5; i++) {            new Thread(ReadWriteLockDemo::readCache, "读线程" + i).start();        }        // 1个写线程(独占,所有读线程等待)        new Thread(() -> updateCache(100), "写线程").start();    }}5. synchronized 的锁升级(偏向锁→轻量级锁→重量级锁)JDK1.6 为优化 synchronized 引入的自适应锁机制,锁级别从低到高升级(不可逆),适配不同并发场景:锁类型核心特点适用场景偏向锁锁偏向第一个线程,无竞争时零开销单线程执行同步代码(如初始化资源)轻量级锁多线程交替竞争,CAS 自旋获取锁少量线程(2-3 个)交替执行重量级锁多线程激烈竞争,线程阻塞(OS 层面)大量线程同时竞争锁6. 自旋锁 vs 阻塞锁类型定义示例适用场景自旋锁获取锁失败时循环重试(自旋),不阻塞CAS、synchronized 轻量级锁锁持有时间短、低冲突(如简单变量更新)阻塞锁获取锁失败时线程阻塞,释放 CPUsynchronized 重量级锁、ReentrantLock锁持有时间长、高冲突(如复杂业务逻辑)7. 分段锁(ConcurrentHashMap JDK1.7)核心定义将数据拆分为多个「段(Segment)」,每个段独立加锁,不同段的操作互不阻塞,并发度 = 段数(默认 16)。适用场景JDK1.7 的 ConcurrentHashMap(JDK1.8 后用 CAS+Synchronized 替代),适用于高并发读写 Map 的场景。三、实战选型指南(核心)业务场景推荐锁类型选型原因简单互斥、代码简洁synchronized隐式锁,无需手动释放,JVM 优化优异可中断 / 超时 / 公平锁ReentrantLock支持灵活的锁控制特性读多写少(缓存 / 查询)ReentrantReadWriteLock/StampedLock读共享,大幅提升读并发效率低冲突、简单变量更新CAS(AtomicInteger/AtomicLong)无锁,性能最高,避免自旋消耗高冲突、写多读少(转账)synchronized/ReentrantLock悲观锁,避免高冲突下的 CAS 自旋 CPU 开销公平性要求高(任务排队)ReentrantLock(true)按请求顺序获取锁,避免线程饥饿高并发 Map 操作ConcurrentHashMapJDK1.8 用 CAS+Synchronized,兼顾性能和安全性总结关键点回顾基础核心:synchronized(简单、通用)和 Lock 接口(灵活、高级)是 Java 锁的两大基石,前者适用于普通场景,后者适用于需要灵活控制的场景;特性选型:读多写少选读写锁,低冲突选乐观锁 / CAS,高冲突选悲观锁,公平性要求高选公平锁;性能原则:优先选择「开销低」的锁(如偏向锁、CAS),高冲突场景才用「高开销」的阻塞锁(如重量级锁);实战建议:90% 的普通业务场景用 synchronized 即可,仅在需要可中断、超时、读写分离时才用 Lock 接口实现类。
  • [技术干货] Java中实现Word和TXT之间互相转换的实用教程【转载】
    Spire.Doc for Java:Word 与 TXT 转换的利器在 Java 生态中,处理 Word 文档的库并不少见,但 Spire.Doc for Java 凭借其强大的功能和易用性脱颖而出。它是一个专业的 Word 文档处理组件,支持创建、读写、编辑、转换和打印 Word 文档,并且兼容多种 Word 版本。其中,对 Word 和 TXT 格式的互相转换提供了非常便捷的 API。引入 Spire.Doc for Java要开始使用 Spire.Doc,您需要将其作为依赖添加到您的 Maven 项目中。Maven 配置示例:1234567891011121314  <repositories>    <repository>        <id>com.e-iceblue</id>        <name>e-iceblue</name>        <url>https://repo.e-iceblue.cn/repository/maven-public/</url>    </repository></repositories><dependencies>    <dependency>        <groupId>e-iceblue</groupId>        <artifactId>spire.doc</artifactId>        <version>14.1.3</version>    </dependency></dependencies>请确保您使用的版本是最新的稳定版本,以获取最佳的兼容性和功能。从 Word 到 TXT:逐步实现文档内容提取将 Word 文档转换为纯文本(TXT)是一个常见的需求,例如用于内容提取、文本分析或跨平台传输。Spire.Doc for Java 提供了一行代码即可完成此操作。实现步骤加载 Word 文档: 使用 Document 类的 loadFromFile() 方法加载目标 Word 文档。保存为 TXT 格式: 调用 saveToFile() 方法,并指定输出路径和 FileFormat.Txt 格式。释放资源: 调用 dispose() 方法释放文档对象占用的资源。Java 代码示例1234567891011121314151617181920import com.spire.doc.Document;import com.spire.doc.FileFormat; public class ConvertWordtoText {     public static void main(String[] args) {         // 创建 Document 对象        Document doc = new Document();         // 加载 Word 文件        doc.loadFromFile("示例.docx");         // 将文档保存为 TXT 格        doc.saveToFile("Word转文本.txt", FileFormat.Txt);         // 释放资源        doc.dispose();    }}代码解析:document.loadFromFile(inputWordPath): 负责读取指定路径的 Word 文档内容。document.saveToFile(outputTxtPath, FileFormat.Txt): 这是转换的核心。它将加载的 Word 文档内容以纯文本格式写入到 outputTxtPath 指定的文件中。FileFormat.Txt 枚举值明确指示了目标格式。document.dispose(): 释放资源,用于关闭文件流并释放内存,特别是在处理大量文档时。从 TXT 到 Word:构建富文本格式文档将纯文本(TXT)文件转换为 Word 文档,通常是为了对其进行格式化、添加图片、表格或其他富文本元素。Spire.Doc 同样能轻松实现这一目标。实现步骤创建或加载 Word 文档: 对于从 TXT 创建新的 Word 文档,直接创建 Document 对象即可。加载 TXT 内容: 使用 Document 类的 loadFromFile() 方法加载 TXT 文件。保存为 Word 格式: 调用 saveToFile() 方法,并指定输出路径和 FileFormat.Docx(或 FileFormat.Doc)格式。释放资源: 调用 dispose() 方法释放文档对象占用的资源。Java 代码示例1234567891011121314151617181920import com.spire.doc.Document;import com.spire.doc.FileFormat; public class ConvertTextToWord {     public static void main(String[] args) {         // 创建 Document 对象        Document txt = new Document();         // 加载 .txt 文本文件        txt.loadFromFile("介绍.txt");         // 将文件保存为 Word 格式        txt.saveToFile("TXT转Word.docx", FileFormat.Docx);         // 释放资源        txt.dispose();    }}代码解析:document.loadFromFile(inputTxtPath): 这里巧妙地利用了 spire.doc for java 的 loadFromFile 方法不仅可以加载 Word 文档,还能加载 TXT 文件并将其内容导入到 Document 对象中。document.saveToFile(outputWordPath, FileFormat.Docx): 将包含 TXT 内容的 Document 对象保存为 Word 格式。FileFormat.Docx 是现代 Word 文档的默认格式,您也可以选择 FileFormat.Doc。格式调整建议:将 TXT 转换为 Word 后,默认情况下可能只是简单的文本导入。如果需要更复杂的格式,例如设置字体、段落样式、页眉页脚等,Spire.Doc 也提供了丰富的 API 来实现这些功能,您可以在 loadFromFile 之后、saveToFile 之前,对 document 对象进行进一步的编辑操作。
  • [技术干货] 一文带你搞懂Java中Error和Exception的区别【转载】
    Error 和 Exception 的基本概念在开始之前,我们先来理解一下这两个概念的基本含义。Error(错误):通常指的是系统级的错误,这些错误往往是程序无法恢复的,或者恢复起来非常困难。比如内存溢出、栈溢出、系统资源耗尽等。这些错误一般不是由程序逻辑问题引起的,而是由系统环境、硬件资源等外部因素导致的。Exception(异常):则是指程序运行过程中出现的异常情况,这些异常通常是可以被程序捕获和处理的。比如数组越界、空指针引用、文件不存在、网络连接失败等。这些异常往往是由程序的设计问题、逻辑错误或者外部输入导致的。简单来说,Error 是"系统说不行",Exception 是"程序说有问题"。两者的核心区别虽然 Error 和 Exception 都是程序运行时的异常情况,但它们有几个关键的区别:严重程度不同Error 通常比 Exception 更严重。Error 往往意味着系统级别的故障,比如内存溢出(OutOfMemoryError)、栈溢出(StackOverflowError)等。这些错误一旦发生,程序通常无法继续正常运行,甚至可能导致整个应用崩溃。Exception 相对来说就没那么严重了。虽然有些 Exception 也会导致程序崩溃(比如未捕获的运行时异常),但大多数 Exception 都是可以被程序捕获和处理的。比如文件读取失败,我们可以提示用户重新选择文件;网络请求失败,我们可以重试或者显示错误信息。处理方式不同对于 Error,我们通常不应该尝试捕获和处理。因为 Error 往往意味着系统资源已经耗尽或者系统环境出现了严重问题,这时候程序已经无法正常工作了,强行处理可能会让问题变得更糟。对于 Exception,我们应该主动捕获和处理。这是程序健壮性的重要体现。比如在读取文件时,我们应该捕获 IOException,然后给用户一个友好的提示,而不是让程序直接崩溃。来源不同Error 通常来自系统层面,比如 JVM 运行时错误、操作系统错误等。这些错误不是由我们的业务代码直接引起的,而是由底层系统或环境问题导致的。Exception 通常来自应用层面,比如我们的业务逻辑、API 调用、数据处理等。这些异常往往可以通过改进代码逻辑、添加校验、重试机制等方式来处理。实际应用场景让我们通过几个实际的场景来理解 Error 和 Exception 的区别:场景一:内存溢出假设你正在开发一个图片处理应用,用户上传了一张非常大的图片。如果你的程序试图将整个图片加载到内存中,而系统内存不足,就会抛出 OutOfMemoryError。这是一个典型的 Error。因为内存不足是系统资源问题,不是你的程序逻辑问题。虽然你可以通过优化代码(比如分块处理图片)来避免这个问题,但一旦内存真的耗尽了,程序就很难恢复了。正确的做法是:在程序设计阶段就考虑内存限制,避免一次性加载过大的数据。如果真的遇到了 OutOfMemoryError,最好的处理方式可能是记录错误日志,然后优雅地退出程序,而不是试图捕获和处理这个错误。场景二:文件读取失败假设你的应用需要读取一个配置文件。如果文件不存在,或者文件被其他程序占用,就会抛出 FileNotFoundException 或 IOException。这是一个典型的 Exception。因为文件读取失败是可以通过程序逻辑来处理的。你可以捕获这个异常,然后给用户一个友好的提示,比如"配置文件不存在,请检查文件路径",或者使用默认配置。123456789101112try {    File configFile = new File("config.properties");    // 读取配置文件} catch (FileNotFoundException e) {    // 文件不存在,使用默认配置    logger.warn("配置文件不存在,使用默认配置");    loadDefaultConfig();} catch (IOException e) {    // 文件读取失败,提示用户    logger.error("读取配置文件失败", e);    showErrorDialog("无法读取配置文件,请检查文件权限");}场景三:网络请求超时假设你的应用需要调用一个远程 API。如果网络连接不稳定,或者服务器响应慢,可能会抛出 SocketTimeoutException 或 ConnectException。这也是一个典型的 Exception。你可以捕获这个异常,然后实现重试机制,或者给用户一个友好的提示。1234567891011121314int maxRetries = 3;for (int i = 0; i < maxRetries; i++) {    try {        // 发送网络请求        return httpClient.execute(request);    } catch (SocketTimeoutException e) {        if (i == maxRetries - 1) {            // 最后一次重试也失败了            throw new ApiException("网络请求超时,请检查网络连接");        }        // 等待一段时间后重试        Thread.sleep(1000 * (i + 1));    }}场景四:空指针引用假设你的代码中有一个对象可能为 null,但你没有做空值检查就直接使用了它,就会抛出 NullPointerException。这也是一个典型的 Exception。虽然 NullPointerException 是运行时异常,不需要强制捕获,但我们应该在代码中主动避免这种情况。1234567891011// 不好的做法String name = user.getName(); // 如果 user 为 null,会抛出 NullPointerExceptionSystem.out.println(name.length()); // 好的做法if (user != null) {    String name = user.getName();    if (name != null) {        System.out.println(name.length());    }}不同语言中的实现虽然 Error 和 Exception 的概念是相通的,但不同语言的实现方式可能不太一样:Java 中,Error 和 Exception 都是 Throwable 的子类。Error 包括 OutOfMemoryError、StackOverflowError 等系统级错误;Exception 包括 RuntimeException(运行时异常)和 CheckedException(检查异常)。Python 中,所有的异常都继承自 BaseException。系统退出异常(SystemExit、KeyboardInterrupt)类似于 Error,其他异常类似于 Exception。Swift 中,Error 是一个协议,任何遵循 Error 协议的类型都可以被抛出。Swift 没有严格区分 Error 和 Exception,但我们可以通过命名和文档来区分系统级错误和应用级异常。最佳实践在实际开发中,我们应该遵循以下原则:对于 Error(系统级错误):不要尝试捕获和处理系统级错误在程序设计阶段就考虑资源限制,避免触发系统错误如果真的遇到了系统错误,记录日志并优雅退出对于 Exception(应用级异常):主动捕获和处理可能出现的异常给用户提供友好的错误提示实现重试机制、降级方案等容错处理记录详细的异常日志,方便问题排查代码设计建议:使用防御性编程,提前检查可能的问题合理使用异常处理,不要过度捕获异常区分可恢复的异常和不可恢复的异常对于关键操作,实现重试和降级机制总结Error 和 Exception 虽然都是程序运行时的异常情况,但它们有本质的区别:Error 是系统级的错误,通常无法恢复,不应该被捕获处理Exception 是应用级的异常,可以被捕获和处理,是程序健壮性的重要体现在实际开发中,我们应该:通过合理的设计避免系统级错误主动捕获和处理应用级异常给用户提供友好的错误提示实现完善的容错和降级机制
  • [技术干货] 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
总条数:764 到第
上滑加载中