• [技术干货] @ModelAttribute、@RequestBody、@RequestParam、@PathVariable 注解对比
    @ModelAttribute@PostMapping("/register")public String register(@ModelAttribute User user) { // 表单数据将自动绑定到 User 对象中 return "userInfo";}@RequestBody@PostMapping("/api/user")public ResponseEntity<?> saveUser(@RequestBody User user) { // JSON 请求体 {"name":"Tom","age":20} return ResponseEntity.ok(user);}@RequestParam@GetMapping("/search")public String search(@RequestParam String keyword) { // /search?keyword=java return keyword;}@PathVariable@GetMapping("/user/{id}")public String getUser(@PathVariable Long id) { return "ID: " + id;}注意,@RequestBody 需要使用 HttpMessageConverter(如 Jackson、FastJson)支持 JSON 解析。转载自https://www.cnblogs.com/yanshajiuzhou/p/18903319
  • [技术干货] Java 创建线程池的几种方式
    一、创建线程池四种方式使用 Executors 类,Executors 类是 Java 中用于创建线程池的工厂类,它提供了多种静态方法来创建不同类型的线程池使用 ThreadPoolExecutor 类,ThreadPoolExecutor 是 Java 中线程池的一个核心类,它提供了更细粒度的控制来创建和管理线程池使用 Future 和 Callable,Future 和 Callable 是并发编程中非常重要的两个接口,它们通常与 ExecutorService 一起使用来执行异步任务。使用 Spring 的 ThreadPooltaskExecutor,ThreadPoolTaskExecutor 是一个基于 java.util.concurrent.ThreadPoolExecutor 的扩展,提供了更丰富的配置选项和与Spring集成的特性二、线程池重要参数corePoolSize (int): 线程池的基本大小,即在没有任务执行时线程池的大小。当新任务提交时,线程池会优先使用已有的空闲线程。maximumPoolSize (int): 线程池能够容纳同时执行的最大线程数。这个参数用于控制线程池的最大规模,防止因任务过多而导致资源耗尽。keepAliveTime (long): 当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程能等待新任务的最长时间。超过这个时间后,多余的线程将被终止。unit (TimeUnit): keepAliveTime 参数的时间单位,常见的时间单位有 TimeUnit.SECONDS、TimeUnit.MINUTES 等。workQueue (BlockingQueue): 一个阻塞队列,用于存储等待执行的任务。常用的阻塞队列有 LinkedBlockingQueue、ArrayBlockingQueue 和 SynchronousQueue 等。threadFactory (ThreadFactory): 用于创建新线程的工厂。可以通过实现 ThreadFactory 接口来自定义线程的创建过程。handler (RejectedExecutionHandler): 当任务太多而线程池无法处理时,用于定义拒绝任务的策略。常见的拒绝策略有 ThreadPoolExecutor.AbortPolicy、ThreadPoolExecutor.CallerRunsPolicy 和 ThreadPoolExecutor.DiscardPolicy 等。package com.demo.threadPool;import java.util.concurrent.*;public class MainDemo1 {    public static void main(String[] args) {        int corePoolSize = 5; // 核心线程数        int maximumPoolSize = 10; // 最大线程数        long keepAliveTime = 1; // 非核心线程空闲存活时间        /**         * 存活时间单位         * TimeUnit.DAYS:天         * TimeUnit.HOURS:小时         * TimeUnit.MINUTES:分         * TimeUnit.SECONDS:秒         * TimeUnit.MILLISECONDS:毫秒         * TimeUnit.MICROSECONDS:微妙         * TimeUnit.NANOSECONDS:纳秒         */        TimeUnit unit = TimeUnit.MINUTES;        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(); // 工作队列        ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);    }}三、线程池5种状态RUNNING:正常运行状态,可接收新任务,可处理阻塞队列中的任务SHUTDOWN:不会接收新任务,但会处理阻塞队列剩余任务STOP:会中断正在执行的任务,并抛弃阻塞队列任务TIDYING:任务全执行完毕,活动线程为 0,即将进入终结TERMINATED:终结状态四、Executors 类创建线程池new newCachedThreadPool():创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池的规模不存在限制。(数量不固定的线程池)new newFixedThreadPool():创建一个固定长度线程池,可控制线程最大并发数,超出的线程会在队列中等待。(固定数量的线程池)new newScheduledThreadPool():创建一个固定长度线程池,支持定时及周期性任务执行。(定时线程池)new newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。(单线程的线程池)固定线程池创建 ( Executors.newFixedThreadPool(5) ):创建一个固定大小的线程池。线程池中的线程数量是固定的,即使有些线程处于空闲状态,它们也不会被回收。package com.demo.threadPool;import java.util.List;import java.util.concurrent.*;public class MainThreadPool {    public static void main(String[] args) throws ExecutionException, InterruptedException {        //初始化固定大小线程池        ExecutorService executor1 = Executors.newFixedThreadPool(5);        //使用 execute(Runnable command) 方法提交一个不需要返回结果的任务,        // 或者使用submit(Callable<T> task) 方法提交一个需要返回结果的任务。        for (int i = 0; i < 10; i++) {            executor1.execute(new TaskR(i));        }        //使用 submit(Callable<T> task) 任务并获取 Future        //使用 Future.get() 方法等待任务完成并获取结果。这个方法会阻塞调用线程直到任务完成。        for (int i = 0; i < 10; i++) {            Future<String> future =  executor1.submit(new TaskC(i));            System.out.println("线程返回结果  "+future.get());        }        // 当所有任务都执行完毕,或者需要关闭线程池时,调用 shutdown() 方法。        // 这将等待正在执行的任务完成,但不接收新任务。        executor1.shutdown();        //使用 shutdownNow() 方法尝试立即停止所有正在执行的任务,并返回等待执行的任务列表        List<Runnable> notExecutedTasks = executor1.shutdownNow();        for(Runnable ls : notExecutedTasks){            System.out.println(ls);        }        //使用 awaitTermination() 方法等待线程池关闭,直到所有任务完成或超时。        boolean res = executor1.awaitTermination(60, TimeUnit.SECONDS);        System.out.println("执行结果:"+res);    }}/** * 实现 Runnable 接口 */class TaskR implements Runnable {    private int id;    public TaskR(int id) {        this.id = id;    }    public void run() {        System.out.println("TaskR " + id + " is running...");    }}/** * 实现 Callable 接口 * 有返回值 */class TaskC implements Callable {    private int id;    public TaskC(int id) {        this.id = id;    }    @Override    public Object call(){        System.out.println("TaskC " + id + " is running...");        return id+"--TaskC";    }}单线程池 (newSingleThreadExecutor):创建一个只有一个线程的线程池。即使有多个任务提交,它们也会被排队,逐个由单个线程执行package com.demo.threadPool;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;/** * 单线程池 (newSingleThreadExecutor): * 创建一个只有一个线程的线程池。即使有多个任务提交,它们也会被排队,逐个由单个线程执行。 */public class MainOne {    public static void main(String[] args) throws ExecutionException, InterruptedException {        /**         * 单线程:创建的执行服务内部有一个线程。所有提交给它的任务将会序列化执行,也就是说,它会在单个线程上依次执行任务,不会有并发执行的情况发生         * 任务队列:如果有多个任务提交给这个执行器,除了当前正在执行的任务外,其他任务将会在一个无界队列中等待,直到线程可用         * 处理任务失败:如果执行中的线程由于任务抛出异常而终止,执行服务会安排一个新的线程来替换它,以继续执行后续的任务         * 使用场景: newSingleThreadExecutor 非常适合需要顺序执行的任务,并且要求任务之间不受并发问题影响的场景         */        ExecutorService executor = Executors.newSingleThreadExecutor();        for (int i = 0; i < 10; i++) {            executor.execute(new TaskR(i));        }        //使用 submit(Callable<T> task) 任务并获取 Future        //使用 Future.get() 方法等待任务完成并获取结果。这个方法会阻塞调用线程直到任务完成。        for (int i = 0; i < 10; i++) {            Future<String> future =  executor.submit(new TaskC(i));            System.out.println("线程返回结果  "+future.get());        }        // 当所有任务都执行完毕,或者需要关闭线程池时,调用 shutdown() 方法。        // 这将等待正在执行的任务完成,但不接收新任务。        executor.shutdown();    }}缓存线程池 (newCachedThreadPool):创建一个可根据需要创建新线程的线程池。如果线程空闲超过60秒,它们将被终止并从池中移除package com.demo.threadPool;import java.util.Date;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * 缓存线程池 (newCachedThreadPool): * 创建一个可根据需要创建新线程的线程池。如果线程空闲超过60秒,它们将被终止并从池中移除 */public class MainCacheThreadPool {    public static void main(String[] args) throws InterruptedException {        System.out.println(Thread.currentThread().getName() + "线程: Start at: " + new Date());        //初始化缓存线程池        ExecutorService exec = Executors.newCachedThreadPool();        for (int i = 1; i < 10; i++) {            System.out.println("添加了第" + i + "个任务类");            Thread.sleep(2000);            exec.execute(new TaskR(i));        }        //所有任务结束后关闭线程池        exec.shutdown();        System.out.println(Thread.currentThread().getName() + " 线程: Finished all threads at:" + new Date());    }}调度线程池 (newScheduledThreadPool):创建一个支持定时任务和周期性任务的线程池package com.demo.threadPool;import java.util.Date;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;/** * 固定频率执行 * 调度线程池 (newScheduledThreadPool): * 创建一个支持定时任务和周期性任务的线程池 */public class MainScheduledThreadPool {    public static void main(String[] args) {        /**         * 场景描述         * 假设你需要一个应用程序,该程序能够每10秒执行一次任务,并在启动后1分钟开始执行。此外,         * 你还需要能够安排一次性任务在未来的某个时间点执行         */        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);        // 安排定期任务        // 初始延迟1分钟,之后每10秒执行一次        threadPool.scheduleAtFixedRate(new TaskR(2), 60, 10, TimeUnit.SECONDS);        // 安排一次性任务        // 使用 schedule 方法安排一个任务,在指定的延迟后执行一次        // 延迟5分钟后执行        threadPool.schedule(new TaskR(3), 5, TimeUnit.MINUTES);        // 关闭线程池        // 当不再需要线程池时,调用 shutdown 方法来关闭线程池。这将等待正在执行的任务完成,但不接收新任务        threadPool.shutdown();        // 等待线程池关闭        // 使用 awaitTermination 方法等待线程池关闭,直到所有任务完成或超时。        try {            threadPool.awaitTermination(1, TimeUnit.HOURS);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}使用给定的线程工厂创建线程池:可以提供一个自定义的 ThreadFactory 来创建线程池中的线程package com.demo.threadPool;import java.util.concurrent.*;/** * 使用给定的线程工厂创建线程池 */public class MainFactory {    public static void main(String[] args) {        //自定义线程工厂创建        ThreadFactory threadFactory = new ThreadFactory() {            @Override            public Thread newThread(Runnable r) {                return new Thread(r);            }        };        //使用给定的线程工厂创建线程池        ExecutorService executor = Executors.newFixedThreadPool(5, threadFactory);        executor.execute(new TaskR(2));    }}自定义线程工厂创建:自定义线程工厂可以设置自己的线程名,设置守护线程,设置线程优先级,处理未捕获的异常等package com.demo.threadPool;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.ThreadFactory;import java.util.concurrent.atomic.AtomicInteger;/** *  自定义线程工厂:设置线程名,守护线程,优先级以及UncaughtExceptionHandler */public class MainFactory implements ThreadFactory {    private final ThreadGroup group;    private final AtomicInteger threadNumber = new AtomicInteger(1);    private final String namePrefix;    public MainFactory(String namePrefix) {        SecurityManager s = System.getSecurityManager();        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();        this.namePrefix = namePrefix + "-thread-";    }    public MainFactory(ThreadGroup group, String namePrefix) {        this.group = group;        this.namePrefix = namePrefix;    }    @Override    public Thread newThread(Runnable r) {        Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);        //守护线程        if (t.isDaemon())            t.setDaemon(true);        //线程优先级        if (t.getPriority() != Thread.NORM_PRIORITY)            t.setPriority(Thread.NORM_PRIORITY);        /**         * 处理未捕捉的异常         */        t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {            @Override            public void uncaughtException(Thread t, Throwable e) {                System.out.println("处理未捕获的异常");            }        });        return t;    }    //测试方法    public static void main(String[] args) {        ExecutorService pool = Executors.newFixedThreadPool(5, new MainFactory("测试线程"));        for (int i = 0; i < 10; i++) {            pool.execute(new Runnable() {                @Override                public void run() {                    System.out.println("线程处理");                    //未捕获的异常,走自定义的UncaughtExceptionHandler逻辑                    int i = 1 / 0;                }            });        }        pool.shutdown();    }}五、ThreadPoolExecutor 类创建线程池ThreadPoolExecutor 是 java.util.concurrent 包中用来创建线程池的一个类。它提供了一种灵活的方式来管理线程池,允许你控制线程的创建和销毁。1ThreadPoolExecutor 类中的几个重要方法execute():向线程池提交一个任务,交由线程池去执行submit():也是向线程池提交任务,但是和execute()方法不同,它能够返回任务执行的结果它实际上还是调用的 execute() 方法,只不过它利用了 Future 来获取任务执行结果invokeAll():提交一个任务集合invokeAny(): 提交一个任务集合,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消shutdown():关闭线程池,再也不会接受新的任务不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止shutdownNow():关闭线程池,再也不会接受新的任务立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务isShutdown():不在 RUNNING 状态的线程池,此方法就返回 trueisTerminated():线程池状态是否是 TERMINATEDpackage com.demo.threadPool;import java.util.Random;import java.util.concurrent.*;/** * ThreadPoolExecutor 是 java.util.concurrent 包中用来创建线程池的一个类 * 它提供了一种灵活的方式来管理线程池,允许你控制线程的创建和销毁。 * 以下是几种常见的创建 ThreadPoolExecutor 线程池的方式 * 实际上 Executors 类也是调用 ThreadPoolExecutor 类创建的线程 */public class MainThreadPoolExecutor {    //测试方法    public static void main(String[] args) {        /**         * 核心线程数,核心线程就是一直存在的线程         */        int corePoolSize = 5;        /**         * 最大线程数,表示线程池中最多能创建多少个线程         * 非核心线程数 = 最大线程数 - 核心线程数         */        int maximumPoolSize = 10;        /**         * 默认情况下,只有当线程池中的线程数大于corePoolSize时,         * keepAliveTime才会起作用,则会终止,直到线程池中的线程数不超过corePoolSize         * 则会终止,直到线程池中的线程数不超过corePoolSize         * 但是如果调用了 allowCoreThreadTimeOut(boolean) 方法         * 在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为 0         * 针对非核心线程而言,表示线程没有任务执行时最多保持多久时间会终止         */        long keepAliveTime = 60;        /**         * 时间单位         * 与 keepAliveTime 配合使用,针对非核心线程         */        TimeUnit unit = TimeUnit.SECONDS;        /**         * 存放任务的阻塞队列         */        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(5);        /**         * 创建线程的工厂,可以为线程创建时起个好名字         */        ThreadFactory threadFactory = new ThreadFactory() {            @Override            public Thread newThread(Runnable r) {                return new Thread(r);            }        };        /**         * 拒绝策略         * 任务太多的时候会进行拒绝操作         * 核心线程,非核心线程,任务队列都放不下时         */        // 自定义拒绝策略        RejectedExecutionHandler defaultHandler1 = new MyRejectedExecutionHandler();        // 默认策略,在需要拒绝任务时抛出RejectedExecutionException        RejectedExecutionHandler defaultHandler3 = new ThreadPoolExecutor.AbortPolicy();        // 直接在 execute 方法的调用线程中运行被拒绝的任务,如果线程池已经关闭,任务将被丢弃;        RejectedExecutionHandler defaultHandler2 = new ThreadPoolExecutor.CallerRunsPolicy();        // 直接丢弃任务        RejectedExecutionHandler defaultHandler4 = new ThreadPoolExecutor.DiscardPolicy();        // 丢弃队列中等待时间最长的任务,并执行当前提交的任务,如果线程池已经关闭,任务将被丢弃        RejectedExecutionHandler defaultHandler5 = new ThreadPoolExecutor.DiscardOldestPolicy();        /**         * 创建线程池         */        ExecutorService service1 =  new ThreadPoolExecutor( corePoolSize, maximumPoolSize,keepAliveTime,                 unit,workQueue,threadFactory,defaultHandler1);        for (int i = 0; i < 10; i++) {            System.out.println("添加第"+i+"个任务");            service1.execute(new MyThread("线程"+i));        }        service1.shutdown();    }}/** * 自定义拒绝策略 */class MyRejectedExecutionHandler implements RejectedExecutionHandler {    @Override    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {        new Thread(r,"新线程"+new Random().nextInt(10)).start();    }}/** * 线程类 */class MyThread implements Runnable {    String name;    public MyThread(String name) {        this.name = name;    }    @Override    public void run() {        try {            Thread.sleep(2000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("线程:"+Thread.currentThread().getName() +" 执行:"+name +"  run");    }}六、Future 和 Callable 类使用创建线程池Callable 是一个函数式接口,它允许你定义一个任务,该任务可以返回一个结果并抛出异常。它是 Runnable 接口的扩展,增加了返回值和抛出异常的能力。返回值:与 Runnable 接口不同,Callable 任务可以返回一个值,返回值通过 Future 对象获取。异常:Callable 任务可以抛出异常,这些异常可以通过 Future 对象处理。Future 接口代表异步计算的结果。它提供了检查计算是否完成的方法,以及获取计算结果的方法。get():获取计算结果。如果计算尚未完成,此方法会阻塞,直到计算完成或抛出异常。isDone():检查计算是否完成。cancel():尝试取消任务。isCancelled():检查任务是否被取消package com.demo.threadPool;import java.util.concurrent.*;/** * Future 使用 */public class MainFuture {    public static void main(String[] args) throws ExecutionException, InterruptedException {        ExecutorService executorService = Executors.newFixedThreadPool(1);        System.out.println("开始时间戳为:" + System.currentTimeMillis());        Future<String> future = executorService.submit(new Test01());        String result = future.get(); //获取计算结果。如果计算尚未完成,此方法会阻塞,直到计算完成或抛出异常        boolean isdone = future.isDone();  //检查计算是否完成        boolean cancel = future.cancel(true);  //尝试取消任务        boolean iscancelled = future.isCancelled(); //检查任务是否被取消        System.out.println("result:"+result);        System.out.println("isdone:"+isdone);        System.out.println("cancel:"+cancel);        System.out.println("iscancelled:"+iscancelled);        System.out.println("结束时间戳为:" + System.currentTimeMillis());     executorService.shutdown();    }}class Test01 implements Callable {    @Override    public Object call() throws Exception {        return "你好";    }}七、Spring 的 ThreadPoolTaskExecutor 类创建线程池ThreadPoolTaskExecutor 是 Spring 框架提供的一个线程池实现,它扩展了 Java 的 ThreadPoolExecutor 并提供了一些额外的配置和功能添加依赖: 如果你的项目是一个 Maven 项目,确保你的 pom.xml 文件中包含了 Spring Boot 的依赖配置线程池: 在 Spring Boot 应用程序中,你可以通过 Java 配置类来配置 ThreadPoolTaskExecutorpackage com.cnpc.epai.assetcatalog.dmp.controller;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.ThreadPoolExecutor;/** * 线程池配置类 */@Configurationpublic class ConfigPoolConfiguration {    @Bean("TaskExecutorDemo")    public ThreadPoolTaskExecutor taskExecutorDemo(){        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();        threadPoolTaskExecutor.setCorePoolSize(10); // 核心线程数        threadPoolTaskExecutor.setMaxPoolSize(20);// 最大线程数        threadPoolTaskExecutor.setQueueCapacity(100); //工作队列        threadPoolTaskExecutor.setKeepAliveSeconds(60); // 非核心线程的空闲存活时间        threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);//指定是否允许核心线程超时。这允许动态增长和收缩,即使与非零队列结合使用也是如此(因为最大池大小只有在队列已满时才会增长)        threadPoolTaskExecutor.setThreadNamePrefix("monitor-thread-pool-");// 设置线程名前缀        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());// 拒绝策略        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);// 设置线程池关闭时需要等待子任务执行完毕,才销毁对应的bean        threadPoolTaskExecutor.initialize();//初始化线程池        return threadPoolTaskExecutor;    }}测试类package com.cnpc.epai.assetcatalog.dmp.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Async;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import org.springframework.stereotype.Service;@Servicepublic class TestService {    @Autowired    private ThreadPoolTaskExecutor taskExecutor;    @Async("taskExecutor")    public void executeTask() {        taskExecutor.execute(() -> {            System.out.println("Executing task in thread: " + Thread.currentThread().getName());        });    }}————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/weixin_39865508/article/details/140520236
  • [技术干货] Java 基础--数组(Array):存储数据的“排排坐”
    一、 数组到底是个啥?简单来说,数组就是一个固定大小的容器 ,里面存放的元素必须是相同的数据类型。几个关键特性要记住:类型统一:一个 int 数组里只能放 int,一个 String 数组里只能放 String。不能混装!长度固定 : 数组一旦创建,它的长度(能装多少个元素)就不能再改变了!这是数组最核心的限制之一 。连续存储 (通常): 在内存中,数组的元素通常是连续存放的,这使得通过索引访问元素非常快 。索引访问 : 每个元素都有一个唯一的索引(编号),从 0 开始!通过索引可以快速定位和访问元素。二、 创建和使用数组 怎么在代码里用数组呢?主要分两步:声明和初始化。2.1 声明数组引用告诉编译器:“我要一个能指向某种类型数组的变量”。// 推荐的声明方式int[] scores;String[] names;double[] prices;// C/C++ 风格的声明方式 (也能用,但不推荐)// int scores[];// String names[];注意:这只是声明了一个引用变量,它现在还是 null,并没有指向任何实际的数组内存空间。2.2 初始化数组(分配空间/赋值)初始化才是真正创建数组对象、分配内存空间的时候。有两种主要方式:方式一:使用 new 指定长度这是最常用的方式,你知道需要多大的数组,但还不确定里面的具体值。// 创建一个能存放 5 个 int 的数组scores = new int[5]; // 分配了 5 个 int 的空间// 创建一个能存放 10 个 String 的数组names = new String[10]; // 分配了 10 个 String 引用的空间// 声明和初始化可以合并double[] salaries = new double[50];重点: 使用 new 创建数组后,里面的元素会被自动赋予默认值:数值类型 (int, double etc.): 0 或 0.0boolean: falsechar: \u0000 (空字符)引用类型 (String, Object etc.): null方式二:静态初始化 (直接提供元素值)如果你在创建数组时就已经知道里面要放哪些元素了,可以用这种更简洁的方式。编译器会根据你提供的值自动确定数组的长度。// 直接提供初始值,长度由编译器确定 (这里是 4)int[] initialScores = {90, 85, 92, 78};// 声明和静态初始化合并String[] weekdays = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"};// 不能分开写!下面这样是错误的 // int[] numbers;// numbers = {1, 2, 3}; // 编译错误// 必须这样写:int[] numbers;numbers = new int[]{1, 2, 3}; // 或者声明时就初始化2.3 访问数组元素:靠索引 [] 数组的核心操作就是通过索引来存取元素。记住:索引从 0 开始! 非常非常重要! public class ArrayAccess {    public static void main(String[] args) {        String[] fruits = {"Apple <>", "Banana <>", "Orange <>"}; // 长度为 3        // 访问元素 (索引 0, 1, 2)        System.out.println("First fruit: " + fruits[0]); // Apple <> (索引 0)        System.out.println("Second fruit: " + fruits[1]); // Banana <> (索引 1)        System.out.println("Third fruit: " + fruits[2]); // Orange <> (索引 2)        // 修改元素        fruits[1] = "Grape <>"; // 把第 2 个元素 (索引 1) 改成 Grape        System.out.println("Second fruit now: " + fruits[1]); // Grape <>        // 尝试访问不存在的索引?后果很严重!        // System.out.println(fruits[3]); // 运行时错误: ArrayIndexOutOfBoundsException    }}2.4 获取数组长度:.length 属性 想知道数组能装多少东西?用 .length 属性(注意:是属性,不是方法,后面没有括号 (),这点和 String 的 length() 方法不同,容易混淆!)。int[] data = new int[10];System.out.println("Length of data array: " + data.length); // Output: 10String[] colors = {"Red", "Green", "Blue"};System.out.println("Number of colors: " + colors.length); // Output: 3.length 在循环遍历数组时特别有用。三、 遍历数组:挨个“点名” 遍历数组就是按顺序访问数组中的每一个元素,通常用循环来实现。3.1 使用传统 for 循环最灵活的方式,因为你可以拿到当前的索引 i。public class ForLoopArray {    public static void main(String[] args) {        double[] readings = {12.5, 13.1, 11.8, 12.9};        System.out.println("Sensor Readings:");        // 循环条件通常是 i < array.length        for (int i = 0; i < readings.length; i++) {            System.out.println("Reading at index " + i + ": " + readings[i]);        }    }}3.2 使用增强型 for 循环 (for-each)如果不需要关心索引,只是想依次处理每个元素,这种方式更简洁、易读。import java.util.ArrayList; // 只是为了演示集合遍历import java.util.List;public class ForEachArray {    public static void main(String[] args) {        char[] vowels = {'a', 'e', 'i', 'o', 'u'};        System.out.print("Vowels: ");        // 依次取出 vowels 数组中的每个 char 赋给变量 vowel        for (char vowel : vowels) {            System.out.print(vowel + " ");        }        System.out.println(); // 输出: a e i o u        // 同样适用于集合 (比如 ArrayList)        // List<String> names = new ArrayList<>(); ...        // for (String name : names) { ... }    }}如何选择? 如果需要索引(比如要根据索引修改元素,或者需要知道当前是第几个元素),用传统 for。如果只是读取每个元素的值,增强型 for 通常更好。四、 数组的“常见烦恼”使用数组时,有几个经典错误你很可能会遇到:ArrayIndexOutOfBoundsException <>: 这是最常见的数组错误!当你试图访问一个不存在的索引时(比如索引 < 0,或者索引 >= array.length),就会抛出这个运行时异常。写循环时要特别小心边界条件!NullPointerException : 如果你的数组引用变量本身是 null(即它没有指向任何数组对象),而你试图访问它的 .length 属性或者通过索引访问元素(如 nullArray[0]),就会得到这个运行时异常。使用数组前,确保它已经被正确初始化了!长度固定不变 <>: 前面强调过,数组长度一旦确定就不能改。如果你的程序需要一个可以动态增删元素的容器,那么 Java 集合框架中的 ArrayList 等类通常是更好的选择。五、 超出基础:多维数组与 Arrays 工具类 ▦5.1 多维数组你可以创建“数组的数组”,最常见的是二维数组,可以把它想象成一个表格或矩阵 。// 创建一个 3行 4列 的 int 二维数组int[][] matrix = new int[3][4];// 静态初始化String[][] board = {    {"X", "O", "X"},    {"O", "X", "O"},    {"X", "O", "X"}};// 访问元素需要两个索引matrix[0][1] = 5; // 设置第 1 行第 2 列 (索引都是从 0 开始)System.out.println("Board[1][1]: " + board[1][1]); // Output: X遍历二维数组通常需要嵌套循环。5.2 java.util.Arrays 工具类 <>Java 提供了一个非常有用的 Arrays 类(在 java.util 包下),里面包含了很多操作数组的static方法,能省不少事:Arrays.toString(array): 把数组转换成易于阅读的字符串形式,方便打印调试,强烈推荐!Arrays.sort(array): 对数组进行排序(原地排序)。Arrays.fill(array, value): 用指定值填充整个数组。Arrays.copyOf(originalArray, newLength): 复制原数组的一部分或全部到一个新的数组(可以用来变相“扩展”数组)。Arrays.equals(array1, array2): 比较两个数组的内容是否相等(注意,不是用 == 比地址)。import java.util.Arrays; // <--- 别忘了导入!public class ArraysUtilDemo {    public static void main(String[] args) {        int[] nums = {5, 2, 8, 1, 9};        // 好看的打印方式        System.out.println("Original array: " + Arrays.toString(nums));        // 排序        Arrays.sort(nums);        System.out.println("Sorted array: " + Arrays.toString(nums));        // 填充        int[] filled = new int[5];        Arrays.fill(filled, -1);        System.out.println("Filled array: " + Arrays.toString(filled));        // 复制 (取前 3 个元素)        int[] copied = Arrays.copyOf(nums, 3); // nums 已经是排序后的 {1, 2, 5, 8, 9}        System.out.println("Copied first 3: " + Arrays.toString(copied));    }}六、总结 数组是 Java 中存储固定数量、相同类型元素的基础数据结构。核心特性:类型统一、长度固定、索引访问 (从 0 开始)。创建方式:new Type[size] 或静态初始化 {...}。访问与长度:使用 array[index] 访问,.length 获取长度。遍历:常用 for 或增强 for 循环。常见陷阱 :ArrayIndexOutOfBoundsException, NullPointerException, 长度固定限制。好帮手 <>:java.util.Arrays 类提供了很多实用方法。虽然 ArrayList 等集合类在灵活性上更胜一筹,但数组在性能(尤其是访问速度)和表示固定大小数据集时仍然有其优势,并且是理解集合类的基础。所以,扎实掌握数组非常重要!七、练练手,检验成果!来,动手写写代码,巩固一下!10 个 double 类型数据的数组 prices,并将其所有元素初始化为 9.99 (使用循环或 Arrays.fill)。给定一个 int 数组 scores = {88, 92, 75, 98, 85},计算并打印数组中所有分数的平均值 (注意结果可能是小数)。查找数组 int[] data = {5, -2, 9, 15, -8, 1} 中的最大值,并打印出来。尝试解释为什么数组的索引是从 0 开始而不是从 1 开始?(提示:可以从内存地址和偏移量角度思考,或说明其历史渊源和编程习惯)比较数组 (int[]) 和 ArrayList<Integer> 的主要区别,尤其是在大小和类型方面。1.初始化 prices 数组:import java.util.Arrays;public class InitPrices {    public static void main(String[] args) {        double[] prices = new double[10];        // 方法一:使用循环        // for (int i = 0; i < prices.length; i++) {        //     prices[i] = 9.99;        // }        // 方法二:使用 Arrays.fill (更推荐)        Arrays.fill(prices, 9.99);        System.out.println("Initialized prices: " + Arrays.toString(prices));    }}2.修改 colors 数组:import java.util.Arrays;public class ModifyColors {    public static void main(String[] args) {        String[] colors = {"Red", "Green", "Blue", "Yellow"};        System.out.println("Original colors: " + Arrays.toString(colors));        // 第三个元素索引是 2        if (colors.length > 2) { // 做个简单检查防止越界            colors[2] = "Purple <>";        }        System.out.println("Modified colors: " + Arrays.toString(colors));        // 输出: Modified colors: [Red, Green, Purple <>, Yellow]    }}循环与计算答案 3.计算平均分:import java.util.Arrays;public class AverageScore {    public static void main(String[] args) {        int[] scores = {88, 92, 75, 98, 85};        if (scores.length == 0) {            System.out.println("Array is empty, cannot calculate average.");            return;        }        int sum = 0;        for (int score : scores) {            sum += score;        }        // 注意:为了得到精确的小数结果,需要将 sum 或 length 转为 double        double average = (double) sum / scores.length;        // 或者 double average = sum * 1.0 / scores.length;        System.out.println("Scores: " + Arrays.toString(scores));        System.out.println("Average score: " + average); // 输出: Average score: 87.6    }}4.查找最大值:import java.util.Arrays;public class FindMaxValue {    public static void main(String[] args) {        int[] data = {5, -2, 9, 15, -8, 1};        if (data.length == 0) {            System.out.println("Array is empty.");            return;        }        int max = data[0]; // 假设第一个元素是最大值        for (int i = 1; i < data.length; i++) {            if (data[i] > max) {                max = data[i]; // 如果找到更大的,更新 max            }        }        System.out.println("Data: " + Arrays.toString(data));        System.out.println("Maximum value: " + max); // 输出: Maximum value: 15    }}概念辨析答案 5.为什么索引从 0 开始? 这主要是历史原因和底层实现效率的考虑。在 C 语言(Java 的重要祖先)及更早的语言中,数组名通常代表数组第一个元素在内存中的起始地址。访问数组元素 array[i],实际上是计算 起始地址 + i * 每个元素的大小 来找到第 i+1 个元素的地址。如果索引从 0 开始,那么访问第一个元素 array[0] 就是 起始地址 + 0 * size,即起始地址本身,计算最简单、最高效。如果从 1 开始,访问第一个元素 array[1] 就需要计算 起始地址 + (1-1) * size,多了一步减法。这种从 0 开始的索引(称为 zero-based indexing)已经成为 C 家族语言(包括 C++, Java, C#, JavaScript 等)的编程惯例。6.int[] vs ArrayList<Integer> 区别:大小:int[]: 大小固定。一旦创建,长度不能改变。ArrayList<Integer>: 大小可变。可以动态添加或删除元素,其内部容量会自动调整(虽然可能涉及性能开销)。类型:int[]: 存储的是基本数据类型 int 的值。ArrayList<Integer>: 存储的是包装类 <font color="purple">Integer</font> 的对象引用。它不能直接存储基本类型 int(但 Java 的自动装箱/拆箱机制使得使用起来很像在存取 int)。功能: ArrayList 提供了更多现成的方法(如 add, remove, contains, size 等),而数组功能相对基础(主要靠索引和 Arrays 工具类)。性能: 对于固定大小且频繁访问的场景,数组通常比 ArrayList 性能稍好(特别是对于基本类型数组,避免了装箱/拆箱开销和对象引用的开销)。————————————————原文链接:https://blog.csdn.net/2401_83830408/article/details/147653824
  • [技术干货] Java中的本地缓存:4种实现方式
    Java中的四种本地缓存技术深度解析与实践在Java开发中,缓存技术是提高应用性能的关键手段之一。今天,我们来聊聊Java中的四种主流本地缓存技术,并通过实例代码帮助大家更好地理解和应用这些技术。一、基础缓存实现首先,我们从最基础的缓存实现讲起。一个简单的缓存系统通常包括缓存实体类、添加、删除、查询和清除缓存的功能。1. 缓存实体类缓存实体类用于存储缓存的键值对以及过期时间。代码如下:。public class CacheEntity {    private String cacheKey;    private Object cacheValue;    private long expireTime; // 过期时间戳     // 构造方法、getter和setter省略}2. 缓存工具类接下来,我们实现一个缓存工具类,使用ConcurrentHashMap作为存储结构,并通过定时任务清除过期数据。import java.util.concurrent.*;import java.util.Map; publicclassCacheUtil {    privatefinalstatic Map<String, CacheEntity> CACHE_MAP = newConcurrentHashMap<>();    privatestaticScheduledExecutorServiceexecutorService= Executors.newSingleThreadScheduledExecutor();     static {        executorService.scheduleAtFixedRate(() -> {            longcurrentTime= System.currentTimeMillis();            CACHE_MAP.values().removeIf(entity -> entity.getExpireTime() < currentTime);        }, 0, 500, TimeUnit.MILLISECONDS);    }     publicstaticvoidput(String key, Object value, long expireTimeInSeconds) {        longexpireTime= System.currentTimeMillis() + expireTimeInSeconds * 1000;        CACHE_MAP.put(key, newCacheEntity(key, value, expireTime));    }     publicstatic Object get(String key) {        CacheEntityentity= CACHE_MAP.get(key);        if (entity == null || entity.getExpireTime() < System.currentTimeMillis()) {            CACHE_MAP.remove(key);            returnnull;        }        return entity.getCacheValue();    }     publicstaticvoiddelete(String key) {        CACHE_MAP.remove(key);    }     publicstaticvoidclear() {        CACHE_MAP.clear();    }}测试代码:public class Test {    public static void main(String[] args) throws InterruptedException {        CacheUtil.put("name", "zzc", 10L);        System.out.println("第一次查询结果:" + CacheUtil.get("name"));        Thread.sleep(2000L);        System.out.println("第二次查询结果:" + CacheUtil.get("name"));    }}二、Guava LoadingCacheGuava是Google提供的一个Java基础库,其中的LoadingCache是一个强大的缓存工具。1. 使用示例import com.google.common.cache.*; import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit; publicclassTest {    publicstaticvoidmain(String[] args)throws ExecutionException {        LoadingCache<String, String> cache = CacheBuilder.newBuilder()                .concurrencyLevel(8)                .expireAfterWrite(10, TimeUnit.SECONDS)                .build(newCacheLoader<String, String>() {                    @Override                    public String load(String key)throws Exception {                        return"default_value";                    }                });         cache.put("name", "zzc");        StringnameValue= cache.get("name");        StringageValue= cache.get("age", () -> "default_age");        StringsexValue= cache.get("sex", () -> "key 不存在");         System.out.println("nameValue: " + nameValue);        System.out.println("ageValue: " + ageValue);        System.out.println("sexValue: " + sexValue);    }}在上面的代码中,当调用cache.get(key)方法时,如果缓存中不存在对应的key,则会通过CacheLoader的load方法加载默认值。三、SpringBoot整合CaffeineCaffeine是一个高性能的Java缓存库,SpringBoot提供了与Caffeine的无缝整合。1. 开启缓存功能在启动类上添加@EnableCaching注解。import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cache.annotation.EnableCaching; @EnableCaching@SpringBootApplicationpublic class TestApplication {    public static void main(String[] args) {        SpringApplication.run(TestApplication.class, args);    }}2. 配置缓存管理器import com.github.benmanes.caffeine.cache.Caffeine;import org.springframework.cache.CacheManager;import org.springframework.cache.annotation.EnableCaching;import org.springframework.cache.caffeine.CaffeineCacheManager;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration; import java.util.concurrent.TimeUnit; @Configuration@EnableCachingpublicclassCacheConfig {     @Bean("caffeineCacheManager")    public CacheManager cacheManager() {        CaffeineCacheManagercacheManager=newCaffeineCacheManager("userCache");        cacheManager.getCache("userCache").getConfig().setCaffeine(caffeineCacheBuilder());        return cacheManager;    }     Caffeine<Object, Object> caffeineCacheBuilder() {        return Caffeine.newBuilder()                .expireAfterWrite(10, TimeUnit.SECONDS)                .maximumSize(100);    }}3. 使用缓存注解import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.Cacheable;import org.springframework.stereotype.Service; @ServicepublicclassUserService {     // 模拟数据库操作    private Map<Integer, User> userMap = newHashMap<>();     @Cacheable(value = "userCache", key = "#id")    public User getUserById(Integer id) {        // 假设从数据库获取用户数据        Useruser=newUser();        user.setId(id);        user.setName("User" + id);        userMap.put(id, user);        return user;    }     @CacheEvict(value = "userCache", key = "#id")    publicvoiddeleteUserById(Integer id) {        userMap.remove(id);    }}四、JetCache——阿里巴巴的分布式缓存框架JetCache是阿里巴巴开源的一款基于Spring和Redis的分布式缓存框架,提供了强大的缓存抽象和注解支持。1. 引入依赖在pom.xml中添加JetCache依赖。<dependency>    <groupId>com.alicp.jetcache</groupId>    <artifactId>jetcache-starter-redis</artifactId>    <version>最新版本号</version></dependency>2. 配置JetCache在application.yml中配置JetCache。jetcache:  stat:enable# 开启统计remote:    default:      type:redis      keyConvertor:fastjson# 序列化方式      valueEncoder:java      valueDecoder:java      poolConfig:        minIdle:5        maxIdle:20        maxTotal:50      host:localhost      port: 63793. 使用JetCache注解import com.alicp.jetcache.anno.CacheType;import com.alicp.jetcache.anno.Cached;import com.alicp.jetcache.anno.CacheUpdate;import com.alicp.jetcache.anno.CacheInvalidate;import org.springframework.stereotype.Service; @ServicepublicclassUserService {    @Cached(name = "userCache", key = "#id", cacheType = CacheType.BOTH)    public String getUser(int id) {        return"用户:" + id;    }     @CacheUpdate(name = "userCache", key = "#id", value = "#user")    publicvoidupdateUser(int id, String user) {        System.out.println("更新用户:" + user);    }     @CacheInvalidate(name = "userCache", key = "#id")    publicvoiddeleteUser(int id) {        System.out.println("删除用户:" + id);    }}JetCache支持本地缓存和远程缓存的组合,非常适合分布式系统。总结今天我们一起探索了Java本地缓存的多种实现方式,从手写缓存到Guava Cache、Caffeine、Ehcache和JetCache。每种方式都有自己的特点和适用场景。————————————————原文链接:https://blog.csdn.net/jackeydengjun/article/details/146579570
  • [技术干货] 深入解析Linux死锁:原理、原因及解决方案
    死锁资源可抢占资源与不可抢占资源的对比特性    可抢占资源    不可抢占资源定义    资源可以被操作系统或其他线程/进程强制抢占    资源不能被操作系统或其他线程/进程抢占例子    CPU时间、内存、网络带宽等    锁、文件描述符、硬件设备控制机制    操作系统的调度器管理    需要显式的同步机制,如锁和信号量死锁风险    较低,因资源可能随时被抢占    较高,特别是在锁竞争时容易死锁死锁死锁(Deadlock) 是一种多线程或多进程程序中的问题,指的是两个或多个线程或进程在执行过程中,由于争夺共享资源而导致的相互等待的状态,从而导致这些线程或进程永远无法继续执行。换句话说,死锁是指系统中各个资源的请求无法得到满足,导致进程停滞不前。死锁的四个必要条件(Coffman等人提出)互斥条件:每个资源要么分配给一个进程(线程),要么就是可用的。占有和等待条件:已经得到某个资源的进程(线程)可以再次请求新的资源。不可抢占条件:已经分配给一个进程(线程)的资源不能强制性地被抢占。环路条件:死锁发生的时候一定由两个或两个以上的进程(线程)组成的一条环路,该环路中的每个进程(线程)都在等待着下一个进程(线程)所占有的资源。死锁建模死锁的具体表现:A请求R,B请求S,C请求T(图a),这是初始的资源请求阶段。资源被分配给进程A、B、C(图b和图c),此时每个进程持有了一个资源。进程A请求S,进程B请求T(图d和图e)。此时,进程A已持有资源R,正在请求资源S;进程B已持有资源S,正在请求资源T;进程C已持有资源T,正在请求资源R。此时发生死锁,因为A、B、C各自等待其他进程释放资源,形成循环等待(图g)四种处理死锁的策略以下是关于四种处理死锁策略的表格形式:策略    描述    优点    缺点死锁预防(Deadlock Prevention)    通过破坏死锁的四个条件之一来避免死锁,例如避免占有并等待、循环等待等条件。    - 可以完全避免死锁的发生。 - 系统始终保持安全状态。    - 可能导致系统效率下降,资源利用率低。 - 限制并发性。死锁避免(Deadlock Avoidance)    通过算法(如银行家算法)动态地判断资源请求是否会导致死锁,只有在安全情况下才会分配资源。    - 比死锁预防更加灵活。 - 可以允许更多的并发。    - 实现复杂,系统开销大,尤其在有大量资源和进程时。死锁检测与恢复(Deadlock Detection and Recovery)    系统允许死锁发生,但会定期检查是否有死锁并采取恢复措施(如回滚进程或终止进程)。    - 实现简单,不需要在资源分配时额外考虑死锁问题。    - 死锁检测和恢复会带来额外的开销。 - 恢复过程中可能需要终止进程,影响系统性能。死锁忽略(Deadlock Ignorance)    假设死锁不会发生,或者即使发生死锁也不去检测和处理。    - 简单实现,系统开销小。    - 死锁发生时可能导致系统停滞或无法继续工作。 - 不适用于高可靠性和高可用性要求的系统。死锁预防破坏互斥条件:在某些情况下,资源可以共享(如读操作)。通过允许多个进程共享资源,可以避免互斥条件。例如,多进程同时读取一个文件,而不进行写操作。破坏占有并等待条件:要求进程一次性请求所有资源:进程在执行前,必须先一次性请求所有所需的资源。如果请求资源的数量大于当前可用资源,进程就会阻塞,直到所有资源都可用。这种方式确保了进程不会在占有资源时再请求新的资源,避免了占有并等待条件。请求资源时不持有其他资源:进程请求一个资源时,必须先释放当前持有的所有资源,直到获得请求的资源。这意味着进程在请求资源时不能持有任何资源。破坏非抢占条件:允许资源抢占:当一个进程请求资源时,如果该资源已被其他进程占用,操作系统可以中断当前持有该资源的进程,强制将资源抢占并分配给请求进程。抢占的资源可以在适当的时机返回给原进程。资源可以被强制回收:如果进程无法获得所需资源,系统可以强制回收当前占用的资源,并重新安排资源分配。破坏循环等待条件:资源请求顺序:规定资源的请求顺序,要求进程按固定顺序请求资源。例如,进程必须先请求资源R,然后请求资源S,依此类推。这样,可以确保不存在形成环路的等待关系,从而避免循环等待条件的发生。每个进程在获取资源时按一个固定的顺序请求资源,这样即使多个进程请求不同的资源,也不会形成等待环路死锁避免(银行家算法)Dijkstra(1965年)提出了一种能够修复死锁的调度算法,称为银行家算法(Banker’s Algorithm)。该模型基于一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度。算法要做的是判断对请求的满足是否会导致进人不安全状态。如果是,就拒绝请求;如果满足请求后系统仍然是安全的,就予以分配。在图中我们看到4个客户A、B、C、D,每个客户都被授予一定数量的贷款单位(比如1单位是1千美元),银行家知道不可能所有客户同时都需要最大贷款额,所以他只保留10个单位而不是22个单位的资金来为客户服务。这里将客户比作进程,贷款单位比作资源,银行家比作操作系统。客户们各自做自己的生意,在某些时刻需要贷款(相当于请求资源)。在某一时刻,具体情况如图b所示。这个状态是安全的,由于保留着2个单位,银行家能够拖延除了c以外的其他请求。因而可以让C先完成,然后释放c所占的4个单位资源。有了这4个单位资源,银行家就可以给D或B分配所需的贷款单位,以此类推。​ 考虑假如向B提供了另一个他所请求的贷款单位,如图b所示,那么我们就有如图c所示的状态,该状态是不安全的。如果忽然所有的客户都请求最大的限额,而银行家无法满足其中任何一个的要求,那么就会产生死锁。不安全状态并不一定引起死锁,由于客户不一定需要其最大贷款额度,但银行家不敢抱这种侥幸心理。​ 银行家算法就是对每一个请求进行检查,检查如果满足这一请求是否会达到安全状态。若是,那么就满足该请求;否则,就推迟对这一请求的满足。为了检查状态是否安全,银行家需要考虑他是否有足够的资源满足某一个客户。如果可以,那么这笔贷款就是能够收回的,并且接着检查最接近最大限额的一个客户,以此类推。如果所有投资最终都能被收回,那么该状态是安全的,最初的请求可以批准。死锁忽略死锁忽略(Deadlock Ignorance)是一种处理死锁问题的策略,在这种策略下,系统选择完全不考虑死锁的发生。换句话说,系统不采取任何措施来检测或预防死锁,而是依赖于系统自然的恢复机制或用户干预来解决死锁。鸵鸟算法鸵鸟算法的核心概念:算法的名字来源于“鸵鸟把头埋进沙子里”这一比喻,意味着系统在遇到问题时选择忽略它,而不是进行积极的处理。该算法的基本思想是每个人在面对问题时都有自己的解决方式,有些人可能选择回避,认为这些问题会自动解决或不值得关注。优缺点:优点    缺点简单实现:不需要复杂的错误处理或检测机制,系统设计简洁。    潜在风险:忽略问题可能会导致系统出现严重故障,难以恢复。提高性能:通过不进行错误检测,减少计算和资源消耗,提升系统效率。    缺乏问题反馈:用户或管理员可能无法及时发现系统问题,延误修复。降低复杂性:简化系统设计,减少错误检测和恢复机制的复杂度。    不适合复杂系统:对于复杂系统,忽略问题可能导致不可预见的风险。适应低风险环境:适用于错误发生概率低、系统能够自行恢复的场景。    用户体验差:系统忽视问题可能影响用户的使用体验,无法及时响应错误。死锁检测和死锁恢复死锁检测方法该方法利用资源的当前分配情况进行死锁检测。资源的分配情况通过矩阵来表示,其中:C表示已分配矩阵(Allocation Matrix):表示每个进程已分配的资源数。B表示请求矩阵(Request Matrix):表示每个进程还需要的资源数。A可用资源向量(Available Resource Vector):表示系统中未被分配的资源数。E表示现有资源向量(Existing resource vector):代表每种已经存在的资源总数。关键恒等式:公式表达为:这条公式的含义是:Cij:表示第i个进程对资源类型j的请求数量。Aj:表示资源类型j已分配给所有进程的数量。Ej:表示资源类型j在系统中的总数。总的来说就是已经分配的资源j的总数加起来的再和所有可供使用的资源总数相加,结果就是该类资源总数。死锁恢复终止进程:强制终止一个或多个进程来释放资源,从而解除死锁。资源回收:强制某些进程释放资源,其他进程可以使用这些资源来继续执行。进程回滚:把某些进程的状态恢复到之前的安全状态,让它们重新开始执行。其他问题两阶段加锁两阶段加锁(Two-Phase Locking, 2PL) 是一种常见的数据库事务控制协议,它在保证事务隔离性的同时避免了死锁的发生。两阶段加锁的基本原理是:一个事务必须首先完成对所需资源的加锁,直到它释放资源前不可以请求更多的锁。两阶段加锁分为两个阶段:扩展阶段(Growing Phase):事务可以不断地请求和获取锁。收缩阶段(Shrinking Phase):一旦事务释放了一个锁,它就不能再申请任何新的锁。例子:银行转账事务假设有两个进程(事务)在银行系统中执行转账操作,涉及两个账户的资源:账户A和账户B。事务1(T1):T1的操作是从账户A转账到账户B,具体过程是:请求锁定账户A,进行扣款。请求锁定账户B,进行存款。提交事务,释放锁。事务2(T2):T2的操作是从账户B转账到账户A,具体过程是:请求锁定账户B,进行扣款。请求锁定账户A,进行存款。提交事务,释放锁。两阶段加锁的过程事务T1:扩展阶段:T1首先请求锁定账户A,成功获取锁。T1继续请求锁定账户B,也成功获得锁。T1没有再申请其他锁,进入收缩阶段,开始释放锁。事务T2:扩展阶段:T2首先请求锁定账户B,成功获取锁。然后,T2尝试请求账户A的锁,但由于T1已经锁定了账户A,T2会被阻塞,等待T1释放锁。T2无法再请求任何新的锁(因为进入了收缩阶段),并且等待T1释放账户A的锁。通信死锁通信死锁(Communication Deadlock) 是指在分布式系统或多进程/多线程系统中,由于进程之间的通信(如消息传递、信号量等)引发的死锁问题。具体来说,当多个进程或线程相互依赖对方的消息或响应,而这些消息或响应无法被发送或接收时,系统可能进入死锁状态。举例解释:生产者-消费者模型中的通信死锁假设我们有一个简单的生产者-消费者问题,其中有两个进程:生产者和消费者。生产者将商品生产到缓冲区中,而消费者从缓冲区中取走商品。生产者和消费者通过共享一个缓冲区进行通信。如果在某些情况下,生产者和消费者由于等待彼此的消息而导致死锁,就会发生通信死锁。步骤1:正常通信生产者(P)生产一个商品,并将其放入缓冲区。消费者(C)从缓冲区中取走一个商品。P 和 C 通过缓冲区的状态来进行通信,P 等待缓冲区有空位置,C 等待缓冲区中有商品。步骤2:发生通信死锁假设存在如下场景:生产者 P 正在等待消费者 C 取走一个商品,以便腾出空间存放新商品(即 P 等待 C 消费)。消费者 C 正在等待生产者 P 放入商品,以便它能够消费(即 C 等待 P 生产)。这种情况下,生产者和消费者彼此等待对方的动作,形成了相互依赖的通信链条,导致死锁发生。步骤3:死锁的发生生产者 P 和消费者 C 永远互相等待对方的动作。生产者没有商品可生产,因为缓冲区已满,消费者无法取走商品。消费者没有商品可消费,因为生产者没有生产新商品。结果,系统进入死锁状态,生产者和消费者都无法继续执行。死锁的原因消息依赖性:生产者和消费者相互依赖对方的消息才能继续执行。例如,生产者必须等待消费者消费商品才能继续生产,而消费者必须等待生产者生产商品才能继续消费。资源共享:当多个进程或线程共享资源(如缓冲区、队列等),并且相互依赖对方的动作时,容易发生死锁。活锁(Livelock)活锁(Livelock) 是一种并发编程中的问题,类似于死锁,但有所不同。与死锁不同,活锁中的进程或线程并没有完全停顿,它们仍然在不断地改变自己的状态,但始终无法达到预期的目标或完成任务。换句话说,活锁是一种进程不停地做出反应,但没有实质性进展的状态。活锁的特点进程不断改变状态:尽管进程没有被完全阻塞,它仍然在进行某些操作(如尝试重新获取资源),但这些操作并没有实质性进展。没有前进:进程虽然不停地变化和响应,但始终没有最终完成任务或获得所需资源。饥饿(Starvation)饥饿(Starvation) 是指在并发系统中,某些进程因为系统资源的分配不公或调度策略不合理,长时间得不到执行,导致它们无法完成任务。饥饿问题通常发生在多个进程争用资源时,其中某些进程总是得不到必要的资源,而一直处于等待状态。饥饿的特点长期等待:某些进程长时间无法获得执行机会,甚至无法获得资源来继续执行。不公平资源分配:由于资源分配策略的不公,某些进程被反复忽略,导致它们得不到满足。进程被“饿死”:虽然进程并没有完全被阻塞或终止,它们的执行被永久推迟,无法正常执行下去。#include<iostream>#include<unistd.h>#include<pthread.h>#include"lock.hpp"  // 这里的lock.hpp是自定义的头文件,假设它包含一些锁的相关定义using namespace std;// 初始化互斥锁pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;#define NUM  10  // 定义线程数量int ticket = 1000;  // 初始票数// 线程执行的函数void* mythread(void* args){    pthread_detach(pthread_self());  // 将当前线程设置为分离线程,线程结束后自动释放资源    uint64_t number = (uint64_t)args;  // 获取传递给线程的参数,转化为线程编号    // 无限循环,直到票数耗尽    while(true)    {        {            pthread_mutex_lock(&lock);  // 上锁,确保只有一个线程在修改票数时能访问临界区                        if(ticket > 0)  // 如果票数大于0,则继续执行            {                usleep(1000);  // 模拟处理过程,暂停1毫秒                cout <<"thread: " << number << " ticket: " << ticket << endl;  // 打印当前线程号和票数                ticket--;  // 减少票数            }            else             {                pthread_mutex_unlock(&lock);  // 解锁,允许其他线程访问临界区                break;  // 如果票数小于或等于0,退出循环,结束线程执行            }                        pthread_mutex_unlock(&lock);  // 解锁,允许其他线程访问临界区        }    }    return nullptr;  // 线程结束}int main(){    // 创建NUM个线程    for(int i = 0; i < NUM; i++)    {        pthread_t tid;        pthread_create(&tid, nullptr, mythread, (void*)i);  // 创建线程并传递线程编号    }    sleep(5);  // 主线程休眠5秒钟,等待子线程执行完成    cout <<"process quit ..." <<endl;  // 主线程结束时输出消息        return 0;}都是9号线程抢占资源。产生了竞争的关系。其他线程解饿。(需要同步解决)NUM; i++){pthread_t tid;pthread_create(&tid, nullptr, mythread, (void*)i); // 创建线程并传递线程编号}————————————————原文链接:https://blog.csdn.net/Cayyyy/article/details/148286969
  • [技术干货] Java SE 枚举类型(Enum)详解
    枚举(Enum)是Java 5引入的一种特殊数据类型,用于定义一组固定的常量。下面我将全面介绍Java SE中枚举的使用方法、特性和最佳实践。一、枚举基础1. 基本定义public enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY}2. 特点类型安全:编译时检查不可实例化:构造函数私有不可继承:final类实现了Comparable和Serializable接口二、枚举的高级用法1. 带属性的枚举public enum Planet { MERCURY(3.303e+23, 2.4397e6), VENUS(4.869e+24, 6.0518e6), EARTH(5.976e+24, 6.37814e6); private final double mass; // in kilograms private final double radius; // in meters Planet(double mass, double radius) { this.mass = mass; this.radius = radius; } public double surfaceGravity() { return G * mass / (radius * radius); }}2. 枚举实现接口public interface Operation { double apply(double x, double y);}public enum BasicOperation implements Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }; private final String symbol; BasicOperation(String symbol) { this.symbol = symbol; }}3. 枚举集合Java提供了专门用于枚举的集合类:EnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);EnumMap<Day, String> activityMap = new EnumMap<>(Day.class);三、枚举的常用方法1. 内置方法values(): 返回枚举数组valueOf(String): 根据名称返回枚举常量name(): 返回枚举常量名称ordinal(): 返回枚举常量序数2. 示例Day[] days = Day.values();Day monday = Day.valueOf("MONDAY");String name = Day.MONDAY.name();int ordinal = Day.MONDAY.ordinal();四、枚举设计模式1. 单例模式public enum Singleton { INSTANCE; public void doSomething() { // ... }}2. 策略模式public enum Calculator { ADD { public int calculate(int a, int b) { return a + b; } }, SUBTRACT { public int calculate(int a, int b) { return a - b; } }; public abstract int calculate(int a, int b);}五、枚举与switch语句枚举非常适合与switch语句配合使用:Day day = Day.MONDAY;switch(day) { case MONDAY: System.out.println("星期一"); break; case FRIDAY: System.out.println("星期五"); break; default: System.out.println("其他日子");}六、枚举的最佳实践优先使用枚举而非int常量更安全,更易读编译时类型检查避免使用ordinal()依赖序数会使代码脆弱考虑使用EnumSet代替位域更类型安全,性能相当大型枚举考虑策略枚举将行为与枚举常量关联七、枚举的性能考虑枚举实例是静态final的,在类加载时创建EnumSet和EnumMap针对枚举做了优化values()方法每次返回新数组(浅拷贝)八、Java 8+中的枚举增强1. lambda表达式public enum Operation { ADD((x, y) -> x + y), SUBTRACT((x, y) -> x - y); private final DoubleBinaryOperator op; Operation(DoubleBinaryOperator op) { this.op = op; } public double apply(double x, double y) { return op.applyAsDouble(x, y); }}2. Stream APIArrays.stream(Day.values()) .filter(d -> d.toString().endsWith("DAY")) .forEach(System.out::println);枚举是Java中强大且灵活的特性,合理使用可以大大提高代码的可读性和健壮性。
  • [技术干货] 深入理解 Java 内存区域与内存溢出异常-转载
    一、引言在 Java 编程领域,内存管理看似由虚拟机自动操持,开发者无需过度介入。然而,当内存泄漏或溢出问题悄然浮现,若对虚拟机内存运作机制缺乏深入认知,排查与修复工作将举步维艰。本文将深入剖析 Java 内存区域,并对常见的内存溢出异常进行详细探讨。二、Java 运行时数据区域(一)程序计数器功能:程序计数器是一块较小的内存空间,其作用是指示当前线程执行字节码的行号,是程序控制流的关键指示器,负责分支、循环、跳转等流程控制。特性:为线程私有,当线程执行 Java 方法时,记录字节码指令地址;执行本地方法时,计数器值为空。该区域不会出现 OutOfMemoryError。(二)Java 虚拟机栈线程私有性:Java 虚拟机栈同样是线程私有的,其生命周期与线程紧密相连。栈帧结构:以栈帧为单位存储方法执行时的局部变量表、操作数栈、动态连接及方法出口等信息。局部变量表存放基本数据类型和对象引用,编译期确定其大小。异常情况:可能抛出 StackOverflowError(当线程请求的栈深度超过虚拟机允许的深度)和 OutOfMemoryError(栈动态扩展时无法申请到足够内存)。(三)本地方法栈功能与虚拟机栈的关系:功能类似虚拟机栈,主要为本地方法执行提供支持。实现方式:实现方式由虚拟机自行决定。异常抛出:在栈深度溢出或扩展失败时,会抛出 StackOverflowError 和 OutOfMemoryError。(四)Java 堆地位与作用:是虚拟机管理的最大内存区域,被所有线程共享,用于存放对象实例,是垃圾收集的主要区域。分代收集理论:基于分代收集理论进行区域划分,可设置为固定或扩展大小。内存溢出情况:当内存不足且无法扩展时,抛出 OutOfMemoryError。(五)方法区线程共享性:线程共享区域,用于存储类型信息、常量、静态变量等。历史变迁:JDK 8 前常被称为 “永久代”,之后采用元空间实现。异常情况:内存不足时抛出 OutOfMemoryError。(六)运行时常量池所属区域:属于方法区,存放编译期生成的字面量与符号引用。动态性:具备动态性,运行时可添加常量。内存异常:内存申请失败时抛出 OutOfMemoryError。(七)直接内存特殊性质:并非虚拟机规范定义区域,NIO 可利用其分配堆外内存以提升性能。内存限制:不受 Java 堆大小限制,但受本机内存制约。溢出异常:超出限制时抛出 OutOfMemoryError。三、内存溢出异常实战(一)Java 堆溢出示例代码:通过不断创建对象耗尽堆内存。import java.util.ArrayList;import java.util.List;class HeapOOM {    static class OOMObject {}    public static void main(String[] args) {        List<OOMObject> list = new ArrayList<>();        while (true) {            list.add(new OOMObject());        }    }}解决思路:可通过 -Xmx 参数设置堆大小,溢出时抛出 OutOfMemoryError: Java heap space,可调整堆大小或优化对象创建逻辑来解决。(二)虚拟机栈溢出示例代码:利用无限递归使栈深度超限。class StackSOF {    private static void stackLeak() {        stackLeak();    }    public static void main(String[] args) {        try {            stackLeak();        } catch (Throwable e) {            System.out.println("Stack depth: " + e.getMessage());            e.printStackTrace();        }    }}解决方法:抛出 StackOverflowError,需检查递归算法并设置终止条件。(三)方法区和运行时常量池溢出示例代码:持续向常量池添加字符串。import java.util.ArrayList;import java.util.List;class MethodAreaOOM {    public static void main(String[] args) {        List<String> list = new ArrayList<>();        int i = 0;        while (true) {            list.add(String.valueOf(i++).intern());        }    }}应对策略:抛出 OutOfMemoryError,需关注常量池使用情况,避免无节制创建常量。(四)本机直接内存溢出示例代码:借助Unsafe类不断分配直接内存。import sun.misc.Unsafe;import java.lang.reflect.Field;class DirectMemoryOOM {    private static final int _1MB = 1024 * 1024;    public static void main(String[] args) throws IllegalAccessException {        Field unsafeField = Unsafe.class.getDeclaredFields()[0];        unsafeField.setAccessible(true);        Unsafe unsafe = (Unsafe) unsafeField.get(null);        while (true) {            unsafe.allocateMemory(_1MB);        }    }}解决方案:抛出 OutOfMemoryError,需合理配置虚拟机参数并监控直接内存使用。四、结语{unsafe.allocateMemory(_1MB);}}}**解决方案**:抛出 `OutOfMemoryError`,需合理配置虚拟机参数并监控直接内存使用。## 四、结语深入掌握 Java 内存区域划分及内存溢出异常原理,是 `Java` 开发者进阶路上的关键。在日常开发中,应养成良好的内存管理习惯,借助工具————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/2401_87533975/article/details/147261259
  • [技术干货] JAVA SE 反射,枚举与lambda表达式 -转载
    📕1. 反射JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。✏️1.1 反射相关的类类名 用途Class类 代表类的实体,在运行的Java应用程序中表示类和接口Field类 代表类的成员变量(类的属性)Method类 代表类的方法Constructor 代表类的构造方法✏️1.2 Class类中的相关方法方法 用途getClassLoader() 获得类的加载器getDeclaredClasses() 返回一个数组,数组中包含该类中所有的类和接口的对象(包含私有的)forName(String className) 根据类名返回类的对象newInstance() 创建类的实例getName() 获得类的完整路径名字✏️1.3 Field类中的相关方法方法 用途getField(String name) 获得某个共有的属性对象getFileds() 获得所有共有的属性对象getDeclaredField(String name) 获得某个属性对象getDeclaredFields() 获得所有属性对象✏️1.4 Method类中的相关方法方法 用途getDeclaredMethod((String name, Class<?>… parameterTypes) 获得该类某个方法getDeclaredMethods() 获得该类的所有方法getMethod((String name, Class<?>… parameterTypes) 获得该类某个公有方法getMethods() 获得该类所有公有方法invoke(对象名,要修改的参数) 触发方法执行的方法✏️1.5 Constructor类中的相关方法方法 用途getConstructor( Class<?>… parameterTypes) 获得该类中与参类型匹配的某个公有构造方法getConstructors() 获得该类中所有的公有构造方法geDeclaredtConstructor(Class<?>… parameterTypes) 获得该类中与参类型匹配的某个构造方法geDeclaredtConstructors() 获得该类中所有的构造方法✏️1.6 获取Class对象的三种方式在反射之前,我们需要做的第一步就是先拿到当前需要反射的类的Class对象,然后通过Class对象的核心方法,达到反射的目的我们先构造出一个学生类作为演示示例class Student {    //私有属性name    private String name = "xiaozhuxiaozhu";    //公有属性age    public int age = 18;    //不带参数的构造⽅法    public Student(){        System.out.println("Student()");    }    private Student(String name,int age) {        this.name = name;        this.age = age;        System.out.println("Student(String,name)");    }    private void eat(){        System.out.println("i am eat");    }    public void sleep(){        System.out.println("i am pig");    }    private void function(String str) {        System.out.println(str);    }    @Override    public String toString() {        return "Student{" +                "name='" + name + '\'' +                ", age=" + age +                '}';    }}1.使用 Class.forName(“类的全路径名”); 静态方法(前提:已明确类的全路径名。 )。public class test1 {    public static void main(String[] args) throws ClassNotFoundException {        //通过 Class 对象的 forName() 静态⽅法来获取,⽤的最多,但可能抛出 ClassNotFoundException 异常        //注意这⾥是类的全路径,如果有包需要加包的路径        Class c1 = Class.forName("reflection.Student");    }}使用 .class 方法(仅适合在编译前就已经明确要操作的 Class)。public class test2 {    public static void main(String[] args) throws ClassNotFoundException {        //直接通过 类名.class 的⽅式得到,该⽅法最为安全可靠,程序性能更⾼        Class c2 = Student.class;    }}使用类对象的 getClass() 方法public class test3 {    public static void main(String[] args) throws ClassNotFoundException {        Student s1 = new Student();        Class c3 = s1.getClass();    }}✏️1.7 反射的使用接下来我们开始使用反射,我们依旧反射上面的Student类注意:所有和反射相关的包都在 import java.lang.reflect 包下面。package reflection;import java.lang.reflect.Method;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;public class demo_test {    public static void main(String[] args) throws ClassNotFoundException,     InstantiationException,     IllegalAccessException,      NoSuchMethodException,     SecurityException,      IllegalArgumentException,      InvocationTargetException,     NoSuchFieldException {        // 获取Class对象        Class<?> c1 = Class.forName("reflection.Student");        Object newInstance = c1.newInstance();;        Student student = (Student)newInstance;        System.out.println(student);        // 获取构造方法        Constructor<?> constructors = c1.getDeclaredConstructor(String.class, int.class);        //设置为true后可修改访问权限        constructors.setAccessible(true);        Object newInstance1 = constructors.newInstance("zhangsan", 20);        Student student1 = (Student)newInstance1;        System.out.println(student1);        // 反射私有⽅法        Method method = c1.getDeclaredMethod("function", String.class);        //设置为true后可修改访问权限        method.setAccessible(true);        method.invoke(student1, "hello");        // 反射私有属性        Field field = c1.getDeclaredField("name");        //设置为true后可修改访问权限        field.setAccessible(true);        field.set(student1, "lisi");        System.out.println(student1);    }}📕2. 枚举枚举是在JDK1.5以后引⼊的。本质是一个特殊的类,是 java.lang.Enum 的⼦类,⾃⼰写的枚举类,就算没有显示的继承 Enum ,但是其默认继承了这个类。枚举的关键字为enum,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等。2.1 枚举的定义限定符 enum 枚举名称{常量1,常量2,常量3....;}✏️2.2 枚举的使用switch语句public enum testenum {    RED,BLACK,WHITE;    public static void main(String[] args) {        testenum t1 = testenum.BLACK;        switch (t1) {            case RED:                System.out.println("红色");                break;            case BLACK:                System.out.println("黑色");                break;            case WHITE:                System.out.println("白色");                break;            default:                break;        }    }   }常⽤⽅法方法名称 描述values() 以数组形式返回枚举类型的所有成员ordinal() 获取枚举成员的索引位置valueOf() 将普通字符串转换为枚举类型compareTo() 比较两个枚举成员在定义时的顺序//将枚举类型的所有成员以数组形式返回并打印testenum[] values = testenum.values();for (int i = 0; i < values.length; i++) {System.out.println(values[i]);    }   ✏️2.3 枚举的构造方法💡💡💡💡💡注意:当枚举对象有参数后,需要提供相应的构造函数💡💡💡💡💡注意:枚举的构造函数默认是私有的 这个⼀定要记住public enum testenum {    RED("红色",1),BLACK("黑色",2),WHITE("白色",3);    private String color;    private int key;    private testenum(String color,int key){        this.color=color;        this.key=key;    }}✏️2.4 枚举与反射关于枚举与反射,请记住一个结论:你不能通过反射获取枚举类的实例!这也是<为什么枚举实现单例模式是安全的?>问题的答案.至于为什么呢,等我学完单例模式之后在来补充…📕3. Lambda表达式Lambda 表达式(Lambda expression),基于数学中的λ演算得名,也可称为闭包(Closure) ,是Java SE 8中⼀个重要的新特性. Lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法⼀样,它提供了⼀个正常的参数列表和⼀个使用这些参数的主体(主体可以是⼀个表达式或⼀个代码块)。✏️3.1 Lambda表达式语法基本语法: (parameters) -> expression 或(parameters) -> {statement;}Lambda表达式由三部分组成:1. paramaters:类似方法中的形参列表,这⾥的参数是函数式接口里的参数。2. ->:可理解为“被用于”的意思3. 方法体:可以是表达式也可以代码块,是函数式接口里方法的实现。代码块可返回一个值或者什么都不反回,这里的代码块块等同于方法的方法体。// 1. 不需要参数,返回值为 2() -> 2// 2. 接收⼀个参数(数字类型),返回其2倍的值x -> 2 * x// 3. 接受2个参数(数字),并返回他们的和(x, y) -> x + y// 4. 接收2个int型整数,返回他们的乘积(int x, int y) -> x * y// 5. 接受⼀个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)(String s) -> System.out.print(s)✏️3.2 函数式接口函数式接口定义:⼀个接口有且只有⼀个抽象方法 。💡💡注意:如果一个接口只有一个抽象方法,那么该接口就是⼀个函数式接口如果我们在某个接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口,这样如果有两个抽象方法,程序编译就会报错的。所以,从某种意义上来说,只要你保证你的接口中只有一个抽象方法,你可以不加这个注解。加上就会自动进行检测的。@FunctionalInterfaceinterface NoParameterNoReturn {//注意:只能有⼀个⽅法void test();}✏️3.3 变量捕获@FunctionalInterfaceinterface NoParameterNoReturn {void test();}public static void main(String[] args) {int a = 10;NoParameterNoReturn noParameterNoReturn = ()->{// a = 99; error————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/2401_82690001/article/details/147606498
  • [技术干货] Java 内存溢出排查优化实战:彻底干掉臭名昭著的 OOM-转载
    Java 内存溢出排查优化实战:彻底干掉臭名昭著的 OOMOutOfMemoryError,也就是臭名昭著的 OOM(内存溢出),相信很多球友都遇到过,相对于常见的业务异常,如数组越界、空指针等,OOM 问题 更难难定位和解决。这篇内容就以之前碰到的一次线上内存溢出的定位、解决问题的方式展开;希望能对碰到类似问题的球友带来思路和帮助。主要从表现-->排查-->定位-->解决 四个步骤来分析和解决问题。内存溢出和内存泄露在 Java 中,和内存相关的问题主要有两种,内存溢出和内存泄漏。内存溢出(Out Of Memory):就是申请内存时,JVM 没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。内存泄露(Memory Leak):就是申请了内存,但是没有释放,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。内存溢出在 JVM 的内存区域中,除了程序计数器,其他的内存区域都有可能发生内存溢出。大家都知道,Java 堆中存储的都是对象,或者叫对象实例,那只要我们不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么就一定会产生内存溢出。比如说运行下面这段代码:public class OOM {    public static void main(String[] args) {        List<Object> list = new ArrayList<>();        while (true) {            list.add(new Object());        }    }}运行程序的时候记得设置一下 VM 参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError,限制堆内存大小为 20M,并且不允许扩展,并且当发生 OOM 时 dump 出当前内存的快照。运行结果如下:请看原文我们在讲运行时数据区的时候也曾讲过。内存泄露内存泄露是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。简单来说,就是应该被垃圾回收的对象没有回收掉,导致占用的内存越来越多,最终导致内存溢出。在上图中:对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长,Y 生命周期结束的时候,垃圾回收器不会回收对象 Y。来看下面的例子:public class MemoryLeak {    public static void main(String[] args) {      try{          Connection conn =null;          Class.forName("com.mysql.jdbc.Driver");          conn =DriverManager.getConnection("url","","");          Statement stmt =conn.createStatement();          ResultSet rs =stmt.executeQuery("....");      } catch(Exception e){//异常日志      } finally {        // 1.关闭结果集 Statement        // 2.关闭声明的对象 ResultSet        // 3.关闭连接 Connection    }  }}创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。这样就会导致内存泄露,最终导致内存溢出。换句话说,内存泄露不是内存溢出,但会加快内存溢出的发生。内存溢出后的表象之前生产环境爆出的内存溢出问题会随着业务量的增长,出现的频次也越来越高。应用程序的业务逻辑非常简单,就是从 Kafka 中将数据消费下来,然后批量的做持久化操作。OOM 现象则是随着 Kafka 的消息越多,出现异常的频次就越快。由于当时还有其他工作所以只能让运维做重启,并且监控好堆内存以及 GC 情况。不得不说,重启大法真的好,能解决大量的问题,但不是长久之计。内存泄露的排查于是我们想根据运维之前收集到的内存数据、GC 日志尝试判断哪里出现了问题。结果发现老年代的内存使用就算是发生 GC 也一直居高不下,而且随着时间推移也越来越高。结合 jstat 的日志发现就算是发生了 FGC,老年代也回收不了,内存已经到顶。甚至有几台应用 FGC 达到了上百次,时间也高的可怕。这说明应用的内存使用肯定是有问题的,有许多赖皮对象始终回收不掉。内存泄露的定位由于生产上的内存 dump 文件非常大,达到了几十 G。也和我们生产环境配置的内存太大有关。所以导致想使用 MAT 分析需要花费大量时间。MAT 是 Eclipse 的一个插件,也可以单独使用,可以用来分析 Java 的堆内存,找出内存泄露的原因。因此我们就想是否可以在本地复现,这样就好定位的多。为了尽快的复现问题,我将本地应用最大堆内存设置为 150M。然后在消费 Kafka 那里 Mock 了一个 while 循环一直不断的生成数据。同时当应用启动之后利用 VisualVM 连上应用实时监控内存、GC 的使用情况。结果跑了 10 几分钟内存使用并没有什么问题。根据图中可以看出,每一次 GC 内存都能有效的回收,所以并没有复现问题。没法复现问题就很难定位。于是我们就采用了一种古老的方法——review 代码,发现生产的逻辑和我们用 while 循环 Mock 的数据还不太一样。果然 review 代码是保障程序性能的第一道防线,诚不欺我。大家在写完代码的时候,尽量也要团队 review 一次。后来查看生产日志发现每次从 Kafka 中取出的都是几百条数据,而我们 Mock 时每次只能产生一条。为了尽可能的模拟生产情况便在服务器上跑了一个生产者程序,一直源源不断的向 Kafka 中发送数据。果然不出意外只跑了一分多钟内存就顶不住了,观察下图发现 GC 的频次非常高,但是内存的回收却是相形见拙。同时后台也开始打印内存溢出了,这样便复现出了问题。内存泄露的解决从目前的表现来看,就是内存中有许多对象一直存在强引用关系导致得不到回收。于是便想看看到底是什么对象占用了这么多的内存,利用 VisualVM 的 HeapDump 功能,就可以立即 dump 出当前应用的内存情况。结果发现 com.lmax.disruptor.RingBuffer 类型的对象占用了将近 50% 的内存。看到这个包自然就想到了 Disruptor 环形队列了。Disruptor 是一个高性能的异步处理框架,它的核心思想是:通过无锁的方式来实现高性能的并发处理,其性能是高于 JDK 的 BlockingQueue 的。再次 review 代码发现:从 Kafka 里取出的 700 条数据是直接往 Disruptor 里丢的。这里也就能说明为什么第一次模拟数据没复现问题了。模拟的时候是一个对象放进队列里,而生产的情况是 700 条数据放进队列里。这个数据量就是 700 倍的差距啊。而 Disruptor 作为一个环形队列,在对象没有被覆盖之前是一直存在的。我也做了一个实验,证明确实如此。我设置队列大小为 8 ,从 0~9 往里面写 10 条数据,当写到 8 的时候就会把之前 0 的位置覆盖掉,后面的以此类推(类似于 HashMap 的取模定位)。所以在生产环境上,假设我们的队列大小是 1024,那么随着系统的运行最终会导致 1024 个位置上装满了对象,而且每个位置都是 700 个!于是查看了生产环境上 Disruptor 的 RingBuffer 配置,结果是:1024*1024。这个数量级就非常吓人了。为了验证是否是这个问题,我在本地将该值设为 2 ,一个最小值试试。同样的 128M 内存,也是通过 Kafka 一直源源不断的取出数据。通过监控如下:跑了 20 几分钟系统一切正常,每当一次 GC 都能回收大部分内存,最终呈现锯齿状。这样问题就找到了,不过生产上这个值具体设置多少还得根据业务情况测试才能知道,但原有的 1024*1024 是绝对不能再使用了。小结虽然到了最后也就改了一行代码(还没改,直接修改配置),但这个排查过程我觉得是很有意义的。也会让大部分觉得 JVM 这样的黑盒难以下手的球友有一个直观感受。同时也得感叹 Disruptor 东西虽好,也不能乱用哦!参考资料内存泄露的排查内存溢出和内存泄露————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/lilinhai548/article/details/147757180
  • [技术干货] Java分层开发必知:PO、BO、DTO、VO、POJO概念详解-转载
    引言  在Java企业级开发中,我们经常会遇到POJO、PO、DTO、BO、VO等各种对象概念,这些看似相似的术语常常让开发者感到困惑。本文将深入解析这些核心概念的区别与联系,并通过代码示例展示它们在实际项目中的正确使用方式。一、核心概念与定义1、PO(Persistent Object,持久化对象)定义:PO与数据库表结构一一对应,每个字段映射表中的一列,通常由ORM框架(如MyBatis、Hibernate)自动生成作用:用于数据持久化操作,如增删改查(CRUD),仅包含数据,不涉及业务逻辑MyBatis示例@Table(name = "t_user")public class UserPO {    @Id    private Long userId;    private String userName;    // 其他字段...} AI写代码java运行2、BO(Business Object,业务对象)定义:BO封装业务逻辑,可由多个PO组合而成,包含复杂的业务操作(如数据校验、流程控制)特点:独立于具体存储方式,可操作数据库、缓存、外部接口等例如,订单BO可能包含用户PO、商品PO和支付信息PO示例场景public class OrderBO {    private OrderPO order;    private List<ItemPO> items;    private UserPO user;    public BigDecimal calculateTotal() {        // 复杂的计算逻辑...    }}AI写代码java运行3、DTO(Data Transfer Object,数据传输对象)定义:DTO用于不同层之间的数据传输,尤其是Service层与Controller层的交互。它可以根据需求封装部分字段,减少不必要的数据传输特点:可能包含多个PO的组合或裁剪后的字段(例如从30个字段中选取10个传输)支持序列化,常用于远程调用(如RPC、HTTP接口)无业务逻辑典型场景public class UserDTO {    private String displayName;    private LocalDateTime registerTime;    // 转换方法    public static UserDTO fromPO(UserPO po) {        // 转换逻辑...    }}AI写代码java运行4、VO(View Object,视图对象)定义:VO是展示层(前端页面)直接使用的对象,仅包含前端需要展示的数据,通常以JSON形式返回应用场景:Controller层将数据封装为VO后传递给前端,避免暴露敏感字段(如密码、内部状态)示例public class UserVO {    private String formattedDate;    private String userLevel;    // 可能包含组合字段...}AI写代码java运行5、POJO(Plain Ordinary Java Object,简单Java对象)定义:POJO是所有简单Java对象的统称,VO、DTO、PO等均属于POJO特点:仅包含属性及Getter/Setter方法,不依赖特定框架二、对比与区别1、表格对比对象    应用场景    特点PO    数据库交互    与数据库表严格对应BO    Service层内部业务逻辑    封装复杂业务逻辑,可包含多个PO的组合DTO    Service层与Controller层间    聚合业务所需数据,可能组合多个POVO    Controller层与前端交互    按前端需求定制字段2、关键区别PO vs DTO:PO严格映射数据库表,DTO可根据业务需求裁剪字段DTO vs VO:DTO关注传输效率,VO关注展示效果。例如,DTO可能包含敏感字段(如用户ID),而VO仅展示脱敏后的信息BO vs PO:BO包含业务逻辑,PO仅存储数据。例如,订单BO可能计算总价,而订单PO仅记录金额3、流转图数据库DAO层Service层Controller层前端/客户端POBODTOVO查询用户信息并返回给前端DAO层通过UserDAO查询数据库,返回UserPOService层将UserPO转换为UserDTO,过滤敏感字段Controller层将UserDTO转换为UserVO,添加前端需要的格式化字段(如日期字符串)总结  合理使用VO、DTO、PO和BO等对象能有效实现解耦、提高灵活性和安全性。VO保护敏感数据,DTO适配不同接口需求,PO确保数据持久化准确,BO封装复杂业务逻辑。在开发中,根据项目复杂度选择合适的对象类型,并统一团队规范,提升代码可读性和可维护性。————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/qq_35512802/article/details/147122534
  • [技术干货] Java大模型开发指南
    一、Java大模型开发概述1.1 什么是大模型大模型(Large Language Model,LLM)是一种基于深度学习的自然语言处理模型,通过海量文本数据训练而成,能够理解和生成人类语言。大模型具有强大的语言理解、生成和推理能力,可以应用于多种场景,如对话系统、内容创作、代码生成等。大模型的发展经历了从BERT、GPT到ChatGPT、Claude等几个重要阶段,模型规模从最初的几亿参数增长到现在的数万亿参数,能力不断增强。1.2 Java在大模型开发中的角色Java作为一门成熟的编程语言,在大模型开发中扮演着重要角色:后端服务开发:构建稳定、高性能的大模型服务API模型部署与集成:将大模型集成到现有Java应用中数据处理与预处理:处理训练数据和用户输入性能优化:优化模型推理速度和资源消耗安全与权限控制:确保模型访问的安全性企业级应用集成:将大模型能力集成到企业级应用中1.3 大模型开发的关键挑战计算资源需求:大模型推理需要大量计算资源响应时间:需要优化推理速度以满足实时应用需求成本控制:API调用和模型部署成本较高安全与隐私:需要保护用户数据和模型安全质量与可靠性:确保模型输出的质量和可靠性可扩展性:支持高并发和水平扩展二、Java大模型开发技术栈2.1 核心框架与库Spring Boot/Spring Cloud:构建微服务架构DJL (Deep Java Library):Java深度学习框架Hugging Face Transformers:通过Java绑定使用预训练模型Apache OpenNLP:自然语言处理工具包Stanford CoreNLP:高级NLP功能Apache Lucene:文本索引和搜索Elasticsearch:分布式搜索引擎LangChain4j:Java版LangChain,用于构建基于大模型的应用Spring AI:Spring生态系统中的AI集成框架Apache Commons Lang:常用工具类库2.2 模型服务与部署Spring AI:Spring生态系统中的AI集成框架TensorFlow Serving:模型服务部署ONNX Runtime:跨平台推理引擎Docker/Kubernetes:容器化部署Spring Native:原生镜像支持Apache OpenWhisk:无服务器平台Spring Cloud Function:函数即服务Vert.x:响应式应用框架Quarkus:Kubernetes原生Java框架Micronaut:现代化JVM框架2.3 数据处理与存储Apache Spark:大规模数据处理Spring Data:数据访问层MongoDB/Elasticsearch:非关系型数据存储Redis:缓存和会话管理Apache Kafka:消息队列和流处理Apache Flink:流处理框架Apache Beam:批处理和流处理统一模型Spring Batch:批处理框架Apache NiFi:数据流自动化Apache Airflow:工作流编排2.4 监控与运维Spring Actuator:应用监控Micrometer:度量收集Prometheus:监控系统Grafana:可视化面板ELK Stack:日志分析Jaeger/Zipkin:分布式追踪Spring Cloud Sleuth:分布式追踪集成Spring Admin:管理界面Spring Boot Admin:应用管理Spring Cloud Config:配置管理三、Java大模型开发流程3.1 需求分析与设计确定应用场景:明确大模型在应用中的具体用途功能规划:定义API接口和功能模块架构设计:设计系统架构,包括模型服务、API网关、数据处理等性能需求:确定响应时间、并发量等性能指标安全需求:确定安全要求和隐私保护措施可扩展性需求:确定系统的扩展性和可维护性要求成本预算:评估开发和运营成本风险评估:识别潜在风险并制定应对策略3.2 模型选择与集成模型评估:根据需求选择合适的预训练模型模型适配:将模型转换为Java可用的格式模型优化:针对特定场景进行微调或优化模型封装:将模型封装为Java服务模型测试:评估模型性能和准确性模型版本管理:建立模型版本管理机制模型监控:实现模型性能和质量监控模型更新策略:制定模型更新和迭代策略3.3 后端服务开发API设计:设计RESTful API或gRPC服务服务实现:使用Spring Boot实现服务逻辑数据处理:实现数据预处理和后处理逻辑错误处理:实现健壮的错误处理机制日志记录:实现详细的日志记录性能优化:优化服务性能安全实现:实现安全措施测试:编写单元测试和集成测试3.4 部署与运维容器化:使用Docker容器化应用编排:使用Kubernetes进行服务编排监控:实现服务监控和日志收集扩展:设计水平扩展策略备份与恢复:实现数据备份和恢复机制灾难恢复:制定灾难恢复计划CI/CD:实现持续集成和持续部署性能测试:进行负载测试和性能测试四、Java大模型开发核心技术4.1 Spring AI集成Spring AI是Spring生态系统中的AI集成框架,简化了Java应用中集成大模型的过程:// 配置Spring AI@Configurationpublic class AiConfig {    @Bean    public OpenAiApi openAiApi() {        return OpenAiApi.builder()                .apiKey(System.getenv("OPENAI_API_KEY"))                .build();    }        @Bean    public AiClient aiClient(OpenAiApi openAiApi) {        return new OpenAiAiClient(openAiApi);    }}// 使用Spring AI@Servicepublic class ChatService {    private final AiClient aiClient;        public ChatService(AiClient aiClient) {        this.aiClient = aiClient;    }        public String generateResponse(String prompt) {        return aiClient.generate(prompt);    }        // 流式响应    public void generateStream(String prompt, Consumer<String> chunkConsumer) {        aiClient.generateStream(prompt, chunkConsumer);    }        // 带参数的生成    public String generateWithParams(String prompt, Map<String, Object> parameters) {        return aiClient.generate(prompt, parameters);    }}4.2 DJL模型加载与推理DJL (Deep Java Library) 是Java深度学习框架,支持加载和推理各种深度学习模型:// 使用DJL加载和推理模型public class ModelService {    private Predictor<String, String> predictor;        public void loadModel() {        // 加载模型        Criteria<String, String> criteria = Criteria.builder()                .setTypes(String.class, String.class)                .optModelPath(Paths.get("model-path"))                .optEngine("PyTorch")                .optProgress(new ProgressBar())                .build();                predictor = ModelZoo.loadModel(criteria);    }        public String predict(String input) {        return predictor.predict(input);    }        // 批量预测    public List<String> batchPredict(List<String> inputs) {        return predictor.batchPredict(inputs);    }        // 带参数的预测    public String predictWithParams(String input, Map<String, Object> parameters) {        return predictor.predict(input, parameters);    }}4.3 异步处理与响应式编程大模型推理通常耗时较长,使用异步处理和响应式编程可以提高系统吞吐量:// 使用Spring WebFlux进行响应式编程@RestController@RequestMapping("/api/chat")public class ChatController {    private final ChatService chatService;        public ChatController(ChatService chatService) {        this.chatService = chatService;    }        @PostMapping    public Mono<ChatResponse> chat(@RequestBody ChatRequest request) {        return Mono.fromCallable(() -> chatService.generateResponse(request.getPrompt()))                .subscribeOn(Schedulers.boundedElastic())                .map(response -> new ChatResponse(response));    }        // 带超时的请求    @PostMapping("/timeout")    public Mono<ChatResponse> chatWithTimeout(@RequestBody ChatRequest request) {        return Mono.fromCallable(() -> chatService.generateResponse(request.getPrompt()))                .subscribeOn(Schedulers.boundedElastic())                .timeout(Duration.ofSeconds(30))                .onErrorResume(TimeoutException.class, e ->                     Mono.just(new ChatResponse("请求超时,请稍后重试")))                .map(response -> new ChatResponse(response));    }        // 带重试的请求    @PostMapping("/retry")    public Mono<ChatResponse> chatWithRetry(@RequestBody ChatRequest request) {        return Mono.fromCallable(() -> chatService.generateResponse(request.getPrompt()))                .subscribeOn(Schedulers.boundedElastic())                .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))                .map(response -> new ChatResponse(response));    }}4.4 流式响应处理对于长文本生成,流式响应可以提供更好的用户体验:// 使用Spring WebFlux实现流式响应@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ServerSentEvent<String>> streamChat(@RequestParam String prompt) {    return Flux.create(sink -> {        chatService.generateStream(prompt, chunk -> {            sink.next(ServerSentEvent.<String>builder()                    .data(chunk)                    .build());        });        sink.complete();    });}// 带错误处理的流式响应@GetMapping(value = "/stream-safe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ServerSentEvent<String>> streamChatSafe(@RequestParam String prompt) {    return Flux.<ServerSentEvent<String>>create(sink -> {        try {            chatService.generateStream(prompt, chunk -> {                sink.next(ServerSentEvent.<String>builder()                        .data(chunk)                        .build());            });            sink.complete();        } catch (Exception e) {            sink.error(e);        }    })    .onErrorResume(e -> Flux.just(        ServerSentEvent.<String>builder()            .data("发生错误: " + e.getMessage())            .build()    ));}4.5 缓存与性能优化使用缓存可以减少重复计算,提高系统性能:// 使用Spring Cache进行缓存@Servicepublic class CachedModelService {    private final ModelService modelService;        public CachedModelService(ModelService modelService) {        this.modelService = modelService;    }        @Cacheable(value = "modelResponses", key = "#input")    public String getCachedResponse(String input) {        return modelService.predict(input);    }        // 带TTL的缓存    @Cacheable(value = "modelResponses", key = "#input", unless = "#result == null")    public String getCachedResponseWithTTL(String input) {        return modelService.predict(input);    }        // 缓存预热    @PostConstruct    public void warmupCache() {        List<String> commonInputs = Arrays.asList(            "常见问题1", "常见问题2", "常见问题3"        );                commonInputs.forEach(this::getCachedResponse);    }}4.6 模型量化与优化模型量化可以减小模型体积,提高推理速度:// 使用DJL进行模型量化public class QuantizedModelService {    private Predictor<String, String> predictor;        public void loadQuantizedModel() {        // 加载量化模型        Criteria<String, String> criteria = Criteria.builder()                .setTypes(String.class, String.class)                .optModelPath(Paths.get("quantized-model-path"))                .optEngine("PyTorch")                .optProgress(new ProgressBar())                .build();                predictor = ModelZoo.loadModel(criteria);    }        public String predict(String input) {        return predictor.predict(input);    }}4.7 多模型集成与路由集成多个模型并根据需求路由请求:// 多模型集成与路由@Servicepublic class MultiModelService {    private final Map<String, ModelService> modelServices;        public MultiModelService(List<ModelService> services) {        modelServices = services.stream()                .collect(Collectors.toMap(ModelService::getModelType, Function.identity()));    }        public String routeRequest(String input, String modelType) {        ModelService service = modelServices.get(modelType);        if (service == null) {            throw new IllegalArgumentException("不支持的模型类型: " + modelType);        }        return service.predict(input);    }        // 智能路由    public String smartRoute(String input) {        // 根据输入内容选择合适的模型        String modelType = selectModelType(input);        return routeRequest(input, modelType);    }        private String selectModelType(String input) {        // 实现模型选择逻辑        if (input.contains("代码")) {            return "code";        } else if (input.contains("图像")) {            return "vision";        } else {            return "general";        }    }}五、Java大模型应用场景5.1 智能客服系统构建基于大模型的智能客服系统,提供24/7自动回答服务:@Servicepublic class CustomerServiceBot {    private final AiClient aiClient;    private final ConversationRepository conversationRepository;        public CustomerServiceBot(AiClient aiClient, ConversationRepository conversationRepository) {        this.aiClient = aiClient;        this.conversationRepository = conversationRepository;    }        public String handleCustomerQuery(String customerId, String query) {        // 获取历史对话        List<Message> history = conversationRepository.findByCustomerId(customerId);                // 构建提示词        String prompt = buildPromptWithHistory(history, query);                // 生成回复        String response = aiClient.generate(prompt);                // 保存对话历史        conversationRepository.save(new Message(customerId, query, response));                return response;    }        private String buildPromptWithHistory(List<Message> history, String query) {        StringBuilder prompt = new StringBuilder();        prompt.append("你是一个专业的客服代表,请根据以下对话历史回答用户的问题:\n\n");                // 添加历史对话        for (Message message : history) {            prompt.append("用户: ").append(message.getQuery()).append("\n");            prompt.append("客服: ").append(message.getResponse()).append("\n\n");        }                // 添加当前问题        prompt.append("用户: ").append(query).append("\n");        prompt.append("客服: ");                return prompt.toString();    }        // 带情感分析的客服    public String handleCustomerQueryWithSentiment(String customerId, String query) {        // 分析用户情感        String sentiment = analyzeSentiment(query);                // 根据情感调整回复风格        String prompt = buildPromptWithSentiment(history, query, sentiment);                // 生成回复        String response = aiClient.generate(prompt);                // 保存对话历史        conversationRepository.save(new Message(customerId, query, response, sentiment));                return response;    }        private String analyzeSentiment(String text) {        // 实现情感分析逻辑        return "positive"; // 或 "negative", "neutral"    }}5.2 代码生成与辅助使用大模型辅助代码生成和重构:@Servicepublic class CodeAssistant {    private final AiClient aiClient;        public CodeAssistant(AiClient aiClient) {        this.aiClient = aiClient;    }        public String generateCode(String description, String language) {        String prompt = String.format("Generate %s code for: %s", language, description);        return aiClient.generate(prompt);    }        public String refactorCode(String code, String instructions) {        String prompt = String.format("Refactor the following code according to these instructions: %s\n\nCode:\n%s",                 instructions, code);        return aiClient.generate(prompt);    }        // 代码解释    public String explainCode(String code) {        String prompt = String.format("Explain the following code in detail:\n\n%s", code);        return aiClient.generate(prompt);    }        // 代码优化    public String optimizeCode(String code) {        String prompt = String.format("Optimize the following code for performance and readability:\n\n%s", code);        return aiClient.generate(prompt);    }        // 单元测试生成    public String generateUnitTests(String code, String language) {        String prompt = String.format("Generate unit tests for the following %s code:\n\n%s", language, code);        return aiClient.generate(prompt);    }}5.3 内容生成与摘要使用大模型生成和摘要内容:@Servicepublic class ContentGenerator {    private final AiClient aiClient;        public ContentGenerator(AiClient aiClient) {        this.aiClient = aiClient;    }        public String generateArticle(String topic, String style) {        String prompt = String.format("Write an article about %s in %s style", topic, style);        return aiClient.generate(prompt);    }        public String summarizeText(String text, int maxLength) {        String prompt = String.format("Summarize the following text in %d words or less:\n\n%s", maxLength, text);        return aiClient.generate(prompt);    }        // 内容分类    public String classifyContent(String content) {        String prompt = String.format("Classify the following content into one of these categories: News, Opinion, Technical, Entertainment, Other\n\n%s", content);        return aiClient.generate(prompt);    }        // 关键词提取    public List<String> extractKeywords(String content) {        String prompt = String.format("Extract the top 5 keywords from the following text:\n\n%s", content);        String response = aiClient.generate(prompt);        return Arrays.asList(response.split(","));    }        // 内容翻译    public String translateContent(String content, String targetLanguage) {        String prompt = String.format("Translate the following text to %s:\n\n%s", targetLanguage, content);        return aiClient.generate(prompt);    }}5.4 多模态应用结合图像和文本的多模态应用:@Servicepublic class ImageCaptionService {    private final AiClient aiClient;    private final ImageProcessor imageProcessor;        public ImageCaptionService(AiClient aiClient, ImageProcessor imageProcessor) {        this.aiClient = aiClient;        this.imageProcessor = imageProcessor;    }        public String generateImageCaption(byte[] imageData) {        // 处理图像        String imageDescription = imageProcessor.processImage(imageData);                // 生成描述        String prompt = "Generate a detailed caption for this image: " + imageDescription;        return aiClient.generate(prompt);    }        // 图像分类    public List<String> classifyImage(byte[] imageData) {        // 处理图像        String imageDescription = imageProcessor.processImage(imageData);                // 分类        String prompt = "Classify this image into categories: " + imageDescription;        String response = aiClient.generate(prompt);        return Arrays.asList(response.split(","));    }        // 图像问答    public String answerImageQuestion(byte[] imageData, String question) {        // 处理图像        String imageDescription = imageProcessor.processImage(imageData);                // 回答问题        String prompt = String.format("Based on this image: %s\n\nAnswer the following question: %s",                 imageDescription, question);        return aiClient.generate(prompt);    }}5.5 数据分析与洞察使用大模型进行数据分析和洞察:@Servicepublic class DataAnalyticsService {    private final AiClient aiClient;        public DataAnalyticsService(AiClient aiClient) {        this.aiClient = aiClient;    }        public String analyzeData(String data, String format) {        String prompt = String.format("Analyze the following %s data and provide insights:\n\n%s", format, data);        return aiClient.generate(prompt);    }        public String generateDataVisualization(String data, String chartType) {        String prompt = String.format("Suggest a %s visualization for the following data:\n\n%s", chartType, data);        return aiClient.generate(prompt);    }        public String detectAnomalies(String timeSeriesData) {        String prompt = String.format("Detect anomalies in the following time series data:\n\n%s", timeSeriesData);        return aiClient.generate(prompt);    }        public String forecastTrends(String historicalData, String metric) {        String prompt = String.format("Forecast future trends for %s based on the following historical data:\n\n%s",                 metric, historicalData);        return aiClient.generate(prompt);    }}六、Java大模型开发最佳实践6.1 模型选择与优化选择合适的模型:根据应用场景选择合适大小的模型模型量化:使用量化技术减小模型体积,提高推理速度模型剪枝:移除不重要的权重,减小模型复杂度批处理优化:使用批处理提高吞吐量模型蒸馏:使用知识蒸馏技术将大模型压缩为小模型模型缓存:缓存常用查询结果模型版本管理:建立模型版本管理机制A/B测试:对不同模型版本进行A/B测试6.2 系统架构设计微服务架构:将模型服务拆分为独立微服务API网关:使用API网关管理服务访问负载均衡:实现模型服务的负载均衡服务发现:使用服务发现机制管理服务实例熔断与降级:实现服务熔断和降级机制异步处理:使用异步处理提高系统吞吐量事件驱动:采用事件驱动架构处理模型请求无服务器架构:考虑使用无服务器架构降低成本6.3 安全与隐私访问控制:实现基于角色的访问控制数据加密:加密敏感数据和模型输入验证:验证和清理用户输入审计日志:记录模型使用情况数据脱敏:对敏感数据进行脱敏处理安全传输:使用HTTPS等安全传输协议模型保护:保护模型知识产权合规性:确保符合数据保护法规6.4 监控与可观测性性能监控:监控模型推理性能资源使用:监控CPU、内存和GPU使用情况错误跟踪:跟踪和记录错误用户反馈:收集用户反馈以改进模型日志聚合:聚合和分析日志数据告警机制:设置性能和质量告警仪表盘:创建监控仪表盘追踪:实现分布式追踪6.5 成本优化按需扩展:根据负载自动扩展资源资源调度:优化资源调度策略缓存策略:实现多级缓存批处理:使用批处理减少API调用模型压缩:压缩模型减小资源消耗冷启动优化:优化模型冷启动时间资源预留:为关键服务预留资源成本监控:监控和分析成本七、Java大模型开发工具与资源7.1 开发工具IntelliJ IDEA:Java IDE,支持Spring开发Spring Tool Suite:基于Eclipse的Spring开发工具Maven/Gradle:依赖管理和构建工具Docker Desktop:容器化开发环境Postman:API测试工具Visual Studio Code:轻量级编辑器,支持Java扩展Jupyter Notebook:交互式开发环境Git:版本控制工具Jenkins/GitLab CI:持续集成工具Kubernetes Dashboard:Kubernetes管理界面7.2 学习资源Spring AI文档:https://docs.spring.io/spring-ai/reference/DJL文档:https://djl.ai/docs/Hugging Face Java API:https://huggingface.co/docs/api-inference/javaSpring Boot文档:https://spring.io/projects/spring-bootKubernetes文档:https://kubernetes.io/docs/LangChain4j文档:https://github.com/langchain4j/langchain4jJava深度学习教程:https://www.deeplearning4j.org/tutorialsSpring Cloud文档:https://spring.io/projects/spring-cloud响应式编程指南:https://projectreactor.io/docs/core/release/reference/Java并发编程:https://docs.oracle.com/javase/tutorial/essential/concurrency/7.3 社区与支持Spring社区:https://spring.io/communityDJL社区:https://djl.ai/communityHugging Face社区:https://huggingface.co/communityStack Overflow:Java和Spring相关问答GitHub:开源项目和示例代码Reddit r/java:Java相关讨论Java User Groups:本地Java用户组Spring论坛:Spring相关问题讨论AI开发者社区:AI开发相关讨论技术博客:Java和AI相关博客7.4 示例项目Spring AI示例:https://github.com/spring-projects/spring-ai/tree/main/spring-ai-samplesDJL示例:https://github.com/deepjavalibrary/djl/tree/master/examplesLangChain4j示例:https://github.com/langchain4j/langchain4j/tree/master/langchain4j-examplesSpring Boot AI示例:https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-samplesKubernetes AI部署示例:https://github.com/kubeflow/kubeflow/tree/master/components/example-notebook-servers八、Java大模型开发未来趋势8.1 技术发展趋势更高效的模型部署:更轻量级的模型和更高效的推理引擎多模态融合:更强大的多模态模型支持边缘计算:在边缘设备上部署大模型联邦学习:分布式模型训练和更新自动机器学习:自动化的模型选择和优化量子计算集成:量子计算与大模型的结合神经符号集成:神经网络与符号推理的结合自监督学习:减少对标注数据的依赖可解释性增强:提高模型决策的可解释性低资源学习:在有限资源下训练和部署模型8.2 应用发展趋势个性化AI助手:更个性化的AI助手和代理行业特定模型:针对特定行业优化的模型低代码/无代码集成:简化大模型集成过程增强现实集成:与AR/VR技术的结合自主系统:更自主的AI系统创意内容生成:更高质量的创意内容生成科学发现辅助:辅助科学研究和发现医疗诊断支持:辅助医疗诊断和治疗教育个性化:个性化学习和教育可持续发展应用:应用于环境和社会可持续发展九、Java大模型开发实战案例9.1 智能客服系统案例项目背景:某电商平台需要构建智能客服系统,处理大量用户咨询。技术方案:使用Spring Boot构建后端服务集成OpenAI API进行对话生成使用Redis缓存常见问题回答使用MongoDB存储对话历史使用Spring Security实现访问控制关键代码:@RestController@RequestMapping("/api/customer-service")public class CustomerServiceController {    private final CustomerServiceBot bot;        @PostMapping("/query")    public ResponseEntity<String> handleQuery(@RequestBody QueryRequest request) {        String response = bot.handleCustomerQuery(request.getCustomerId(), request.getQuery());        return ResponseEntity.ok(response);    }        @GetMapping("/history/{customerId}")    public ResponseEntity<List<Message>> getHistory(@PathVariable String customerId) {        List<Message> history = conversationRepository.findByCustomerId(customerId);        return ResponseEntity.ok(history);    }}实现效果:系统能够处理80%的常见问题响应时间平均在2秒以内用户满意度提升30%9.2 代码生成助手案例项目背景:某软件开发公司需要提高开发效率,构建代码生成助手。技术方案:使用Spring Boot构建API服务集成Codex API进行代码生成使用Spring Cache缓存常用代码片段使用GitHub API集成版本控制使用Docker容器化部署关键代码:@Servicepublic class CodeGenerationService {    private final AiClient aiClient;    private final CodeRepository codeRepository;        public CodeSnippet generateCodeSnippet(String description, String language) {        // 检查缓存        String cacheKey = language + ":" + description;        CodeSnippet cached = codeCache.get(cacheKey);        if (cached != null) {            return cached;        }                // 生成代码        String code = aiClient.generate("Generate " + language + " code for: " + description);                // 创建代码片段        CodeSnippet snippet = new CodeSnippet(description, code, language);                // 保存到缓存和数据库        codeCache.put(cacheKey, snippet);        codeRepository.save(snippet);                return snippet;    }}实现效果:开发效率提升25%代码质量一致性提高新员工上手速度加快9.3 内容生成平台案例项目背景:某内容平台需要自动生成高质量文章,减少人工创作成本。技术方案:使用Spring Cloud构建微服务架构集成GPT-4 API进行内容生成使用Elasticsearch进行内容索引和搜索使用Kafka进行事件驱动处理使用Kubernetes进行容器编排关键代码:@Servicepublic class ContentGenerationService {    private final AiClient aiClient;    private final ContentRepository contentRepository;    private final KafkaTemplate<String, ContentEvent> kafkaTemplate;        public Content generateContent(ContentRequest request) {        // 生成内容        String content = aiClient.generate(buildPrompt(request));                // 创建内容对象        Content contentObj = new Content(            request.getTitle(),            content,            request.getCategory(),            request.getAuthor()        );                // 保存到数据库        contentRepository.save(contentObj);                // 发送事件        kafkaTemplate.send("content-events", new ContentEvent(contentObj, "CREATED"));                return contentObj;    }        private String buildPrompt(ContentRequest request) {        return String.format(            "Write a %s article about %s with title '%s'. " +            "The article should be informative, engaging, and approximately %d words long.",            request.getStyle(),            request.getTopic(),            request.getTitle(),            request.getWordCount()        );    }}实现效果:内容生成成本降低60%内容产量增加300%内容质量评分保持在4.2/5以上十、常见问题与解决方案10.1 性能问题问题:大模型推理速度慢,影响用户体验。解决方案:使用模型量化和剪枝减小模型体积实现多级缓存减少重复计算使用异步处理和流式响应采用批处理提高吞吐量使用GPU加速推理过程10.2 成本问题问题:API调用成本高,难以控制预算。解决方案:实现请求限流和配额管理使用缓存减少API调用采用混合模型策略,简单问题使用小模型实现成本监控和告警优化提示词减少token使用10.3 质量问题问题:模型输出质量不稳定,有时产生错误或无关内容。解决方案:实现输出验证和过滤机制使用更高质量的提示词工程实现人工审核流程收集用户反馈持续改进使用特定领域微调模型10.4 安全与隐私问题问题:模型可能泄露敏感信息或产生有害内容。解决方案:实现输入和输出过滤使用内容安全API检测有害内容实现数据脱敏和匿名化建立安全审计和监控制定明确的使用政策10.5 可扩展性问题问题:系统难以应对流量增长和需求变化。解决方案:采用微服务架构提高灵活性实现自动扩缩容使用负载均衡分散流量采用事件驱动架构解耦服务实现服务降级和熔断机制十一、总结Java大模型开发是一个快速发展的领域,结合Java的稳定性和大模型的强大能力,可以构建各种智能应用。通过掌握Spring AI、DJL等工具,以及异步处理、流式响应等技术,开发者可以高效地构建和部署大模型应用。随着技术的不断发展,Java大模型开发将变得更加简单和高效,为开发者提供更多可能性。通过遵循最佳实践,关注安全与性能,开发者可以构建出高质量的大模型应用,满足各种业务需求————————————————原文链接:https://blog.csdn.net/weixin_74417835/article/details/147067060
  • [技术干货] Java-断点续传
    什么是断点续传?引用百度百科:对断点续传的定义个人理解:如果我们要在项目的媒资管理部分,上传视频文件(通常这类视频文件都比较大),http协议本身对上传文件大小没有限制,但是客户的网络环境质量,电脑硬件环境参差不齐,可能会导致一个大文件快上传完了出现断网的情况,从而导致文件没有上传成功,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求是能做到 断点续传断点续传流程图如下图:简要概述实现步骤:前端上传完先把文件分成块一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传各分块上传完成最后在服务端合并文件(为了方便理解,我用Java代码的方法 测试文件的分块与合并)先进行大文件资源 分块操作:1.导入一系列的包package com.xuecheng.media; import org.apache.commons.codec.digest.DigestUtils;import org.apache.commons.io.IOUtils;import org.junit.jupiter.api.Test; import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.RandomAccessFile;import java.util.*;导入多个外部和Java自带的库,其中org.apache.commons.codec.digest.DigestUtils:用于生成文章的摘要,能够在断点续传中验证文件的完整性2.定义相关的类和方法public class BigFileTest {    //测试文件分块方法    @Test    public void testChunk() throws IOException {定义一个公共类BigFileTest,用于包含文件处理相关的测试方法,并且定义了一个testChunk方法,该方法用于执行文件分块操作,并声明了可能抛出IOException异常,因为文件操作可能会失败3.创建分块文件File sourceFile = new File("d:/develop/bigfile_test/nacos.mp4");String chunkPath = "d:/develop/bigfile_test/chunk/";File chunkFolder = new File(chunkPath);if (!chunkFolder.exists()) {    chunkFolder.mkdirs();}创建一个File对象,表示要对d:/develop/bigfile_test/路径下的nacos.mp4源文件进行分块,并创建一个File对象,表示分块存储的文件夹,如果分块文件夹不存在,则创建它 4.对大文件分成文件块//分块大小long chunkSize = 1024 * 1024 * 1;//分块数量long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize);System.out.println("分块总数:" + chunkNum);然后对 文件进行分割,定义每个分块的大小,通过源文件的长度除以分块大小(要采用向上取整噢)使用Math.ceil方法向上取整,计算出分块的总数5.进行对文件的读取写入//缓冲区大小byte[] b = new byte[1024];//使用RandomAccessFile访问文件RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");//分块for (int i = 0; i < chunkNum; i++) {    //创建分块文件    File file = new File(chunkPath + i);    if (file.exists()) {        file.delete();    }    boolean newFile = file.createNewFile();    if (newFile) {        //向分块文件中写数据        RandomAccessFile raf_write = new RandomAccessFile(file, "rw");        int len = -1;        while ((len = raf_read.read(b))!= -1) {            raf_write.write(b, 0, len);            if (file.length() >= chunkSize) {                break;            }        }        raf_write.close();        System.out.println("完成分块" + i);    }}raf_read.close();然后进行缓冲区与文件读取写入,以上代码块创建了一个大小为1024字节的缓冲区数组b,用于读取源文件数据,并且以只读模式打开源文件,然后进行分块操作循环分块数量chunkNum(对源文件进行分块后得到的分块总数),if循环判断分块文件是否创建成功,如果创建成功,则从源文件中读取数据到缓冲区,直到读取到文件末尾再进行分块文件 合并操作:各个文件块读取完毕后,就可以进行 文件合并 操作,分块合并步骤File chunkFolder = new File("D:\\...\\chunk\\");  // 分块文件目录File sourceFile = new File("D:\\...\\星际牛仔1.mp4");  // 原始文件File mergeFile = new File("D:\\...\\星际牛仔1-1.mp4");  // 合并后的文件mergeFile.createNewFile();  // 创建空的目标文件定义分块文件的存储目录,原始文件路径和合并后的文件路径,并创建一个空的目标文件RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");  // 以读写模式打开合并文件byte[] buffer = new byte[1024];  // 读写缓冲区(1KB)初始化写入流,RandomAccessFile是一个用于随机访问文件的类,适合大文件操作,“rw”表示读写模式,byte[ ] buffer = new byte[1024]这段代码创建了一个字节数组,作为读写缓冲区(临时存储从文件或网络中读取的数据),每次读取1KB数据,减少磁盘I/O次数//listFiles()表示返回目录中所有文件和子目录的File数组,files是包含所有分块文件的数组File[] files = chunkFolder.listFiles();//将数组转换为list集合,fileList是包含所以分块文件的list<File>List<File> fileList = Arrays.asList(files);//然后对List进行排序,Comparator.comparingInt创建一个比较器,按照指定规则排序(将文件名转换为整数进行排序)Collections.sort(fileList, Comparator.comparingInt(o -> Integer.parseInt(o.getName())));对分块文件进行排序,o -> Integer.parseInt(o.getName()):是一个Lambda表达式,用于将文件名转换为整数进行排序//循环遍历分块文件列表for (File chunkFile : fileList) {//以只读模式打开分块文件 "r"表示以只读模式打开    RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r"); //读取分块文件内容并写入合并文件    int len;    while ((len = raf_read.read(buffer)) != -1) {          raf_write.write(buffer, 0, len);      }    raf_read.close();  // 关闭分块文件流}raf_write.close();  // 关闭合并文件流这段代码遍历分块文件列表,并以只读模式打开分块文件,然后读取分块文件内容并写入到合并文件,最后关闭分块文件流和合并文件流这里我再总结一下掌握断点续传的技术,我们能够将多个分块文件合并成一个完整文件,避免用户在下载过程因为网络,系统等一系列原因导致下载中断,而需重新开始下载的情况。而是可以直接从中断处继续下载,节省了我们时间和网络资源————————————————原文链接:https://blog.csdn.net/2301_79757798/article/details/145633400
  • [技术干货] java内部类赋值_详解 Java 内部类
    内部类在 Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java 中内部类的相关知识点和一些使用内部类时需要注意的点。从种类上说,内部类可以分为四类:普通内部类、静态内部类、匿名内部类、局部内部类。我们来一个个看:普通内部类这个是最常见的内部类之一了,其定义也很简单,在一个类里面作为类的一个字段直接定义就可以了,例:public class InnerClassTest {public class InnerClassA {}}在这里 InnerClassA 类为 InnerClassTest 类的普通内部类,在这种定义方式下,普通内部类对象依赖外部类对象而存在,即在创建一个普通内部类对象时首先需要创建其外部类对象,我们在创建上面代码中的 InnerClassA 对象时先要创建 InnerClassTest 对象,例:public class InnerClassTest {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;public InnerClassTest() {// 在外部类对象内部,直接通过 new InnerClass(); 创建内部类对象InnerClassA innerObj = new InnerClassA();System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1);System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2);System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3);System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4);}public class InnerClassA {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;//        static int field5 = 5; // 编译错误!普通内部类中不能定义 static 属性public InnerClassA() {System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");System.out.println("其外部类的 field1 字段的值为: " + field1);System.out.println("其外部类的 field2 字段的值为: " + field2);System.out.println("其外部类的 field3 字段的值为: " + field3);System.out.println("其外部类的 field4 字段的值为: " + field4);}}public static void main(String[] args) {InnerClassTest outerObj = new InnerClassTest();// 不在外部类内部,使用:外部类对象. new 内部类构造器(); 的方式创建内部类对象//        InnerClassA innerObj = outerObj.new InnerClassA();}}这里的内部类就像外部类声明的一个属性字段一样,因此其的对象时依附于外部类对象而存在的,我们来看一下结果:我们注意到,内部类对象可以访问外部类对象中所有访问权限的字段,同时,外部类对象也可以通过内部类的对象引用来访问内部类中定义的所有访问权限的字段,后面我们将从源码里面分析具体的原因。我们下面来看一下静态内部类。静态内部类我们知道,一个类的静态成员独立于这个类的任何一个对象存在,只要在具有访问权限的地方,我们就可以通过 类名.静态成员名 的形式来访问这个静态成员,同样的,静态内部类也是作为一个外部类的静态成员而存在,创建一个类的静态内部类对象不需要依赖其外部类对象。例:public class InnerClassTest {public int field1 = 1;public InnerClassTest() {System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");// 创建静态内部类对象StaticClass innerObj = new StaticClass();System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1);System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2);System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3);System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4);}static class StaticClass {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;// 静态内部类中可以定义 static 属性static int field5 = 5;public StaticClass() {System.out.println("创建 " + StaticClass.class.getSimpleName() + " 对象");//            System.out.println("其外部类的 field1 字段的值为: " + field1); // 编译错误!!}}public static void main(String[] args) {// 无需依赖外部类对象,直接创建内部类对象//        InnerClassTest.StaticClass staticClassObj = new InnerClassTest.StaticClass();InnerClassTest outerObj = new InnerClassTest();}}结果:可以看到,静态内部类就像外部类的一个静态成员一样,创建其对象无需依赖外部类对象(访问一个类的静态成员也无需依赖这个类的对象,因为它是独立于所有类的对象的)。但是于此同时,静态内部类中也无法访问外部类的非静态成员,因为外部类的非静态成员是属于每一个外部类对象的,而本身静态内部类就是独立外部类对象存在的,所以静态内部类不能访问外部类的非静态成员,而外部类依然可以访问静态内部类对象的所有访问权限的成员,这一点和普通内部类无异。匿名内部类匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了:public class InnerClassTest {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;public InnerClassTest() {System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");}// 自定义接口interface OnClickListener {void onClick(Object obj);}private void anonymousClassTest() {// 在这个过程中会新建一个匿名内部类对象,// 这个匿名内部类实现了 OnClickListener 接口并重写 onClick 方法OnClickListener clickListener = new OnClickListener() {// 可以在内部类中定义属性,但是只能在当前内部类中使用,// 无法在外部类中使用,因为外部类无法获取当前匿名内部类的类名,// 也就无法创建匿名内部类的对象int field = 1;@Overridepublic void onClick(Object obj) {System.out.println("对象 " + obj + " 被点击");System.out.println("其外部类的 field1 字段的值为: " + field1);System.out.println("其外部类的 field2 字段的值为: " + field2);System.out.println("其外部类的 field3 字段的值为: " + field3);System.out.println("其外部类的 field4 字段的值为: " + field4);}};// new Object() 过程会新建一个匿名内部类,继承于 Object 类,// 并重写了 toString() 方法clickListener.onClick(new Object() {@Overridepublic String toString() {return "obj1";}});}public static void main(String[] args) {InnerClassTest outObj = new InnerClassTest();outObj.anonymousClassTest();}}来看看结果:上面的代码中展示了常见的两种使用匿名内部类的情况:直接 new 一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象并赋值给前面的 OnClickListener 类型的引用;new 一个已经存在的类 / 抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。同样的,在匿名内部类中可以使用外部类的属性,但是外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。局部内部类局部内部类使用的比较少,其声明在一个方法体 / 一段代码块的内部,而且不在定义类的定义域之内便无法使用,其提供的功能使用匿名内部类都可以实现,而本身匿名内部类可以写得比它更简洁,因此局部内部类用的比较少。来看一个局部内部类的小例子:public class InnerClassTest {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;public InnerClassTest() {System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");}private void localInnerClassTest() {// 局部内部类 A,只能在当前方法中使用class A {// static int field = 1; // 编译错误!局部内部类中不能定义 static 字段public A() {System.out.println("创建 " + A.class.getSimpleName() + " 对象");System.out.println("其外部类的 field1 字段的值为: " + field1);System.out.println("其外部类的 field2 字段的值为: " + field2);System.out.println("其外部类的 field3 字段的值为: " + field3);System.out.println("其外部类的 field4 字段的值为: " + field4);}}A a = new A();if (true) {// 局部内部类 B,只能在当前代码块中使用class B {public B() {System.out.println("创建 " + B.class.getSimpleName() + " 对象");System.out.println("其外部类的 field1 字段的值为: " + field1);System.out.println("其外部类的 field2 字段的值为: " + field2);System.out.println("其外部类的 field3 字段的值为: " + field3);System.out.println("其外部类的 field4 字段的值为: " + field4);}}B b = new B();}//        B b1 = new B(); // 编译错误!不在类 B 的定义域内,找不到类 B,}public static void main(String[] args) {InnerClassTest outObj = new InnerClassTest();outObj.localInnerClassTest();}}同样的,在局部内部类里面可以访问外部类对象的所有访问权限的字段,而外部类却不能访问局部内部类中定义的字段,因为局部内部类的定义只在其特定的方法体 / 代码块中有效,一旦出了这个定义域,那么其定义就失效了,就像代码注释中描述的那样,即外部类不能获取局部内部类的对象,因而无法访问局部内部类的字段。最后看看运行结果:内部类的嵌套内部类的嵌套,即为内部类中再定义内部类,这个问题从内部类的分类角度去考虑比较合适:普通内部类:在这里我们可以把它看成一个外部类的普通成员方法,在其内部可以定义普通内部类(嵌套的普通内部类),但是无法定义 static 修饰的内部类,就像你无法在成员方法中定义 static 类型的变量一样,当然也可以定义匿名内部类和局部内部类;静态内部类:因为这个类独立于外部类对象而存在,我们完全可以将其拿出来,去掉修饰它的 static 关键字,他就是一个完整的类,因此在静态内部类内部可以定义普通内部类,也可以定义静态内部类,同时也可以定义 static 成员;匿名内部类:和普通内部类一样,定义的普通内部类只能在这个匿名内部类中使用,定义的局部内部类只能在对应定义域内使用;局部内部类:和匿名内部类一样,但是嵌套定义的内部类只能在对应定义域内使用。深入理解内部类不知道小伙伴们对上面的代码有没有产生疑惑:非静态内部类可以访问外部类所有访问权限修饰的字段(即包括了 private 权限的),同时,外部类也可以访问内部类的所有访问权限修饰的字段。而我们知道,private 权限的字段只能被当前类本身访问。然而在上面我们确实在代码中直接访问了对应外部类 / 内部类的 private 权限的字段,要解除这个疑惑,只能从编译出来的类下手了,为了简便,这里采用下面的代码进行测试:public class InnerClassTest {int field1 = 1;private int field2 = 2;public InnerClassTest() {InnerClassA inner = new InnerClassA();int v = inner.x2;}public class InnerClassA {int x1 = field1;private int x2 = field2;}}我在外部类中定义了一个默认访问权限(同一个包内的类可以访问)的字段 field1, 和一个 private 权限的字段 field2 ,并且定义了一个内部类 InnerClassA ,并且在这个内部类中也同样定义了两个和外部类中定义的相同修饰权限的字段,并且访问了外部类对应的字段。最后在外部类的构造方法中我定义了一个方法内变量赋值为内部类中 private 权限的字段。我们用 javac 命令(javac InnerClassTest.java)编译这个 .java 文件,会得到两个 .classs 文件。InnerClassTest.class 和 InnerClassTest$InnerClassA.class,我们再用 javap -c 命令(javap -c InnerClassTest 和 javap -c InnerClassTest$InnerClassA)分别反编译这两个 .class 文件,InnerClassTest.class 的字节码如下:我们注意到字节码中多了一个默认修饰权限并且名为 access$100 的静态方法,其接受一个 InnerClassTest 类型的参数,即其接受一个外部类对象作为参数,方法内部用三条指令取到参数对象的 field2 字段的值并返回。由此,我们现在大概能猜到内部类对象是怎么取到外部类的 private 权限的字段了:就是通过这个外部类提供的静态方法。类似的,我们注意到 24 行字节码指令 invokestatic ,这里代表执行了一个静态方法,而后面的注释也写的很清楚,调用的是 InnerClassTest$InnerClassA.access$000 方法,即调用了内部类中名为 access$000 的静态方法,根据我们上面的外部类字节码规律,我们也能猜到这个方法就是内部类编译过程中编译器自动生成的,那么我们赶紧来看一下 InnerClassTest$InnerClassA 类的字节码吧:果然,我们在这里发现了名为 access$000 的静态方法,并且这个静态方法接受一个 InnerClassTest$InnerClassA 类型的参数,方法的作用也很简单:返回参数代表的内部类对象的 x2 字段值。我们还注意到编译器给内部类提供了一个接受 InnerClassTest 类型对象(即外部类对象)的构造方法,内部类本身还定义了一个名为 this$0 的 InnerClassTest 类型的引用,这个引用在构造方法中指向了参数所对应的外部类对象。最后,我们在 25 行字节码指令发现:内部类的构造方法通过 invokestatic 指令执行外部类的 access$100 静态方法(在 InnerClassTest 的字节码中已经介绍了)得到外部类对象的 field2 字段的值,并且在后面赋值给 x2 字段。这样的话内部类就成功的通过外部类提供的静态方法得到了对应外部类对象的 field2 。上面我们只是对普通内部类进行了分析,但其实匿名内部类和局部内部类的原理和普通内部类是类似的,只是在访问上有些不同:外部类无法访问匿名内部类和局部内部类对象的字段,因为外部类根本就不知道匿名内部类 / 局部内部类的类型信息(匿名内部类的类名被隐匿,局部内部类只能在定义域内使用)。但是匿名内部类和局部内部类却可以访问外部类的私有成员,原理也是通过外部类提供的静态方法来得到对应外部类对象的私有成员的值。而对于静态内部类来说,因为其实独立于外部类对象而存在,因此编译器不会为静态内部类对象提供外部类对象的引用,因为静态内部类对象的创建根本不需要外部类对象支持。但是外部类对象还是可以访问静态内部类对象的私有成员,因为外部类可以知道静态内部类的类型信息,即可以得到静态内部类的对象,那么就可以通过静态内部类提供的静态方法来获得对应的私有成员值。来看一个简单的代码证明:public class InnerClassTest {int field1 = 1;private int field2 = 2;public InnerClassTest() {InnerClassA inner = new InnerClassA();int v = inner.x2;}// 这里改成了静态内部类,因而不能访问外部类的非静态成员public static class InnerClassA {private int x2 = 0;}}同样的编译步骤,得到了两个 .class 文件,这里看一下内部类的 .class 文件反编译的字节码 InnerClassTest$InnerClassA:仔细看一下,确实没有找到指向外部类对象的引用,编译器只为这个静态内部类提供了一个无参构造方法。而且因为外部类对象需要访问当前类的私有成员,编译器给这个静态内部类生成了一个名为 access$000 的静态方法,作用已不用我多说了。如果我们不看类名,这个类完全可以作为一个普通的外部类来看,这正是静态内部类和其余的内部类的区别所在:静态内部类对象不依赖其外部类对象存在,而其余的内部类对象必须依赖其外部类对象而存在。OK,到这里问题都得到了解释:在非静态内部类访问外部类私有成员 / 外部类访问内部类私有成员 的时候,对应的外部类 / 外部类会生成一个静态方法,用来返回对应私有成员的值,而对应外部类对象 / 内部类对象通过调用其内部类 / 外部类提供的静态方法来获取对应的私有成员的值。内部类和多重继承我们已经知道,Java 中的类不允许多重继承,也就是说 Java 中的类只能有一个直接父类,而 Java 本身提供了内部类的机制,这是否可以在一定程度上弥补 Java 不允许多重继承的缺陷呢?我们这样来思考这个问题:假设我们有三个基类分别为 A、B、C,我们希望有一个类 D 达成这样的功能:通过这个 D 类的对象,可以同时产生 A 、B 、C 类的对象,通过刚刚的内部类的介绍,我们也应该想到了怎么完成这个需求了,创建一个类 D.java:class A {}class B {}class C {}public class D extends A {// 内部类,继承 B 类class InnerClassB extends B {}// 内部类,继承 C 类class InnerClassC extends C {}// 生成一个 B 类对象public B makeB() {return new InnerClassB();}// 生成一个 C 类对象public C makeC() {return new InnerClassC();}public static void testA(A a) {// ...}public static void testB(B b) {// ...}public static void testC(C c) {// ...}public static void main(String[] args) {D d = new D();testA(d);testB(d.makeB());testC(d.makeC());}}程序正确运行。而且因为普通内部类可以访问外部类的所有成员并且外部类也可以访问普通内部类的所有成员,因此这种方式在某种程度上可以说是 Java 多重继承的一种实现机制。但是这种方法也是有一定代价的,首先这种结构在一定程度上破坏了类结构,一般来说,建议一个 .java 文件只包含一个类,除非两个类之间有非常明确的依赖关系(比如说某种汽车和其专用型号的轮子),或者说一个类本来就是为了辅助另一个类而存在的(比如说上篇文章介绍的 HashMap 类和其内部用于遍历其元素的 HashIterator 类),那么这个时候使用内部类会有较好代码结构和实现效果。而在其他情况,将类分开写会有较好的代码可读性和代码维护性。内部类和内存泄露在这一小节开始前介绍一下什么是内存泄露:即指在内存中存在一些其内存空间可以被回收的对象因为某些原因又没有被回收,因此产生了内存泄露,如果应用程序频繁发生内存泄露可能会产生很严重的后果(内存中可用的空间不足导致程序崩溃,甚至导致整个系统卡死)。听起来怪吓人的,这个问题在一些需要开发者手动申请和释放内存的编程语言(C/C++)中会比较容易产生,因为开发者申请的内存需要手动释放,如果忘记了就会导致内存泄露,举个简单的例子(C++):#include int main() {// 申请一段内存,空间为 100 个 int 元素所占的字节数int *p = new int[100];// C++ 11p = nullptr;return 0;}在这段代码里我有意而为之:在为指针 p 申请完内存之后将其直接赋值为 nullptr ,这是 C++ 11 中一个表示空指针的关键字,我们平时常用的 NULL 只是一个值为 0 的常量值,在进行方法重载传参的时候可能会引起混淆。之后我直接返回了,虽然在程序结束之后操作系统会回收我们程序中申请的内存,但是不可否认的是上面的代码确实产生了内存泄露(申请的 100 个 int 元素所占的内存无法被回收)。这只是一个最简单不过的例子。我们在写这类程序的时候当动态申请的内存不再使用时,应该要主动释放申请的内存:#include int main() {// 申请一段内存,空间为 100 个 int 元素所占的字节数int *p = new int[100];// 释放 p 指针所指向的内存空间delete[] p;// C++ 11p = nullptr;return 0;}而在 Java 中,因为 JVM 有垃圾回收功能,对于我们自己创建的对象无需手动回收这些对象的内存空间,这种机制确实在一定程度上减轻了开发者的负担,但是也增加了开发者对 JVM 垃圾回收机制的依赖性,从某个方面来说,也是弱化了开发者防止内存泄露的意识。当然,JVM 的垃圾回收机制的利是远远大于弊的,只是我们在开发过程中不应该丧失了这种对象和内存的意识。回到正题,内部类和内存泄露又有什么关系呢?在继续阅读之前,请确保你对 JVM 的在进行垃圾回收时如何找出内存中不再需要的对象有一定的了解,如果你对这个过程不太了解,你可以参考一下 这篇文章 中对这个过程的简单介绍。我们在上面已经知道了,创建非静态内部类的对象时,新建的非静态内部类对象会持有对外部类对象的引用,这个我们在上面的源码反编译中已经介绍过了,正是因为非静态内部类对象会持有外部类对象的引用,因此如果说这个非静态内部类对象因为某些原因无法被回收,就会导致这个外部类对象也无法被回收,这个听起来是有道理的,因为我们在上文也已经介绍了:非静态内部类对象依赖于外部类对象而存在,所以内部类对象没被回收,其外部类对象自然也不能被回收。但是可能存在这种情况:非静态内部类对象在某个时刻已经不在被使用,或者说这个内部类对象可以在不影响程序正确运行的情况下被回收,而因为我们对这个内部类的使用不当而使得其无法被 JVM 回收,同时会导致其外部类对象无法被回收,即为发生内存泄露。那么这个 “使用不当” 具体指的是哪个方面呢?看一个简单的例子,新建一个 MemoryLeakTest 的类:public class MemoryLeakTest {// 抽象类,模拟一些组件的基类abstract static class Component {final void create() {onCreate();}final void destroy() {onDestroy();}// 子类实现,模拟组件创建的过程abstract void onCreate();// 子类实现,模拟组件摧毁的过程abstract void onDestroy();}// 具体某个组件static class MyComponent extends Component {// 组件中窗口的单击事件监听器static OnClickListener clickListener;// 模拟组件中的窗口MyWindow myWindow;@Overridevoid onCreate() {// 执行组件内一些资源初始化的代码clickListener = new OnClickListener() {@Overridepublic void onClick(Object obj) {System.out.println("对象 " + obj + " 被单击");}};// 新建我的窗口对象,并设置其单击事件监听器myWindow = new MyWindow();myWindow.setClickListener(clickListener);}@Overridevoid onDestroy() {// 执行组件内一些资源回收的代码myWindow.removeClickListener();}}// 我的窗口类,模拟一个可视化控件static class MyWindow {OnClickListener clickListener;// 设置当前控件的单击事件监听器void setClickListener(OnClickListener clickListener) {this.clickListener = clickListener;}// 移除当前控件的单击事件监听器void removeClickListener() {this.clickListener = null;}}// 对象的单击事件的监听接口public interface OnClickListener {void onClick(Object obj);}public static void main(String[] args) {MyComponent myComponent = new MyComponent();myComponent.create();myComponent.destroy();// myComponent 引用置为 null,排除它的干扰myComponent = null;// 调用 JVM 的垃圾回收动作,回收无用对象System.gc();System.out.println("");}}我们在代码中添加一些断点,然后采用 debug 模式查看:程序执行到 72 行代码,此时 72 行代码还未执行,因此 myComponent 引用和其对象还未创建,继续执行:这里成功创建了一个 MyComponent 对象,但是其 create 方法还未执行,所以 myWindow 字段为 null,这里可能有小伙伴会问了,myComponent 对象的 clickListener 字段呢?怎么不见了?其实这和我们在代码中定义 clickListener 字段的形式有关,我们定义的是 static OnClickListener clickListener; ,因此 clickListener 是一个静态字段,其在类加载的完成的时候储存在 JVM 中内存区域的 方法区 中,而创建的 Java 对象储存在 JVM 的堆内存中,两者不在同一块内存区域。关于这些细节,想深入了解的小伙伴建议阅读《深入理解JVM虚拟机》。好了,我们继续执行代码:myComponent.create 方法执行完成之后创建了 OnClickListener 内部类对象,并且为 myWindow 对象设置 OnCLickListener 单击事件监听。我们继续:myComponent.destroy 方法执行完成之后,myWindow.removeClickListener 方法也执行完成,此时 myWindow 对象中的 clickListener字段为 null。我们继续:代码执行到了 80 行,在此之前,所有的代码和解释都没有什么难度,跟着运行图走,一切都那么顺利成章,其实这张图的运行结果也很好理解,只不过图中的文字需要思考一下:myComponent 引用指向的对象真的被回收了吗?要解答这个问题,我们需要借助 Java 中提供的内存分析工具 jvisualvm (以前它还不叫这个名字…),它一般在你安装 JDK 的目录下的 bin 子目录下:我们运行这个程序:在程序左边可以找到我们当前正在执行的 Java 进程,双击进入:单击 tab 中的 监视 选项卡,可以看到当前正在执行的 Java 进程的一些资源占用信息,当然我们现在的主要目的是分析内存,那么们单击右上角的 堆 Dump :在这个界面,单击 类 选项卡,会出现当前 Java 进程中用到的所有的类,我们已经知道我们要查找的类的对象只创建了一个,因此我们根据右上角的 实例数 来进行排除:我们成功的找到了我们创建的对象!而这样也意味着当我们在上面代码中调用 JVM 的垃圾回收动作没有回收这三个对象,这其实就是一个真真切切的内存泄露!因为我们将 main 方法中的 myComponent 引用赋值为 null,就意味着我们已经不再使用这个组件和里面的一些子组件(MyWindow 对象),即这个组件和其内部的一些组件应该被回收。但是调用 JVM 的垃圾回收却并没有将其对应的对象回收。造成这个问题的原因在哪呢?其实就在于我们刚刚在 MyComponent 类中定义的 clickListener 字段,我们在代码中将其定义成了 static 类型的,同时这个字段又指向了一个匿名内部类对象(在 create 方法中 创建了一个 OnClickListener 接口对象,即通过一个匿名内部类实现这个接口并创建其对象),根据 JVM 寻找和标记无用对象的规则(可达性分析算法),其会将 clickListener 字段作为一个 “root” ,并通过它来寻找还有用的对象,在这个例子中,clickListener 字段指向一个匿名内部类对象,这个匿名内部类对象有一个外部类对象(MyComponent 类型的对象)的引用,而外部类对象中又有一个 MyWindow 类型的对象引用。因此 JVM 会将这三个对象都视为有用的对象不会回收。用图来解释吧:Ok,通过这个过程,相信你已经理解了造成此次内存泄露的原因了,那么我们该如何解决呢?对于当前这个例子,我们只需要改一些代码:把 MyComponent 类中的 clickListener 字段前面的 static 修饰符去掉就可以了(static OnClickListener clickListener; -> OnClickListener clickListener;),这样的话 clickListener 指向的对象,就作为 MyComponent 类的对象的一部分了,在 MyComponent 对象被回收时里面的子组件也会被回收。同时它们之间也只是互相引用(MyComponent 外部类对象中有一个指向 OnClickListener 内部类对象的引用,OnClickListener 内部类对象有一个指向 MyComponent 外部类对象的引用),根据 JVM 的 “可达性分析” 算法,在两个对象都不再被外部使用时,JVM 的垃圾回收机制是可以标记并回收这两个对象的。 虽然不强制要求你在 MyComponent 类中的 onDestroy 方法中将其 clickListener 引用赋值为 null,但是我还是建议你这样做,因为这样更能确保你的程序的安全性(减少发生内存泄露的机率,毕竟匿名内部类对象会持有外部类对象的引用),在某个组件被销毁时将其内部的一些子组件进行合理的处理是一个很好的习惯。你也可以自定义一个静态内部类或者是另外自定义一个类文件,并实现 OnClickListener 接口,之后通过这个类创建对象,这样就可以避免通过非静态内部类的形式创建 OnClickListener 对象增加内存泄露的可能性。避免内存泄漏那么我们在日常开发中怎么合理的使用内部类来避免产生内存泄露呢?这里给出一点我个人的理解:能用静态内部类就尽量使用静态内部类,从上文中我们也知道了,静态内部类的对象创建不依赖外部类对象,即静态内部对象不会持有外部类对象的引用,自然不会因为静态内部类对象而导致内存泄露,所以如果你的内部类中不需要访问外部类中的一些非 static 成员,那么请把这个内部类改造成静态内部类;对于一些自定义类的对象,慎用 static 关键字修饰(除非这个类的对象的声明周期确实应该很长),我们已经知道,JVM 在进行垃圾回收时会将 static 关键字修饰的一些静态字段作为 “root” 来进行存活对象的查找,所以程序中 static 修饰的对象越多,对应的 “root” 也就越多,每一次 JVM 能回收的对象就越少。 当然这并不是建议你不使用 static 关键字,只是在使用这个关键字之前可以考虑一下这个对象使用 static 关键字修饰对程序的执行确实更有利吗?为某些组件(大型)提供一个当这个大型组件需要被回收的时候用于合理处理其中的一些小组件的方法(例如上面代码中 MyComponent 的 onDestroy 方法),在这个方法中,确保正确的处理一些需要处理的对象(将某些引用置为 null、释放一些其他(CPU…)资源)————————————————原文链接:https://blog.csdn.net/weixin_35998791/article/details/114450261
  • [技术干货] JAVA SE 多线程
    首先,我们设想以下的一个场景:当一家公司去银行办理业务,既要进行财务转账,⼜要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五⼀起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理同一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)。为什么要有线程呢?首先, “并发编程” 成为 “刚需” ,单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源 . 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要用到并发编程.其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量创建线程比创建进程更快.销毁线程比销毁进程更快.调度线程比调度进程更快最后, 线程虽然比进程轻量, 但是人们还不满足 , 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)进程和线程的区别?进程是包含线程的. 每个进程至少有一个线程存在,即主线程。进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.进程是系统分配资源的最小单位,线程是系统调度的最小单位。一个进程挂了一般不会影响到其他进程. 但是一个线程挂了, 可能把同进程内的其他线程一起带走(整个进程崩溃).Java 的线程 和 操作系统线程 的关系?线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用 , Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.1. Thread类及常见方法Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread 对象与之关联。用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。1.1 创建线程继承Thread类//继承Thread来创建一个线程类class MyThread extends Thread{    @Override    //重新run方法,run方法中是该线程具体要做的任务    public void run() {        for (int i = 0; i < 100; i++) {            System.out.println(i);        }    }}public class Test {    public static void main(String[] args) {        //实例化线程类对象        MyThread t = new MyThread();        //通过start()方法启动线程        t.start();    }}实现 Runnable 接口class MyRunnable implements Runnable{    @Override    public void run() {        for (int i = 0; i < 100; i++) {            System.out.println(i);        }    }}public class Test {    public static void main(String[] args){        //创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数        Thread t = new Thread(new MyRunnable());        t.start();    }}匿名内部类创建 Thread 子类对象public class Test {    public static void main(String[] args) {        //匿名内部类创建Thread的子类        Thread t = new Thread(){            @Override            public void run() {                for (int i = 0; i < 100; i++) {                    System.out.println(i);                }            }        };        t.start();    }}匿名内部类创建 Runnable 子类对象public class Test {    public static void main(String[] args) {        Thread t = new Thread(new Runnable() {            @Override            public void run() {                for (int i = 0; i < 100; i++) {                    System.out.println(i);                }            }        });        t.start();    }}lambda 表达式创建 Runnable 子类对象public class Test {    public static void main(String[] args) {        Thread t = new Thread(()->{            System.out.println("这是一个用lambda表达式创建的线程");        });        t.start();    }}强烈推荐!!!1.2 Thread 的常见构造方法方法 说明Thread() 创建线程对象Thread(String name) 创建线程对象并命名Thread(Runnable target , String name) 使用Runnable对象创建线程对象并命名Thread(Runnable target) 使用Runnable对象创建线程对象1.3 Thread 的几个常见属性• ID 是线程的唯一标识,不同线程不会重复• 名称是什么无所谓,不影响运行,是为了方便调试• 状态表示线程当前所处的一个情况• 优先级高的线程理论上来说更容易被调度到• 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。• 是否存活,即简单的理解,为 run 方法是否运行结束了1.4 启动一个线程—start()我们现在已经知道如何通过覆写 run 方法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。• 覆写 run 方法是提供给线程要做的事情的指令清单• 线程对象可以认为是把 李四、王五叫过来了• 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。调用 start 方法, 才真的在操作系统的底层创建出一个线程.1.5 中断一个线程—interrupt()通过一个变量进行标记public class Test {    public static boolean flag = true;    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(){            @Override            public void run() {                while(flag){                    System.out.println("hello thread");                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        };        t.start();        System.out.println("hello main");        Thread.sleep(3000);        flag = false;        System.out.println("让线程中断");    }}调用 interrupt() 方法public class Test {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(()->{            // 由于这个 currentThread 方法, 是在后续 t start 之后, 才执行的.            // 并且是在 t 线程中执行的. 返回的结果就是指向 t 线程对象的引用了.            while(!Thread.currentThread().isInterrupted()){                System.out.println("hello thread");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                    break;                }            }        });        t.start();        Thread.sleep(2000);        //调用这个方法,就是把标志位由false改为true        t.interrupt();    }}//使用interrupt()方法的时候//1. t线程没有进行sleep()阻塞时,t的isInterrupted()方法返回true,通过循环条件结束循环//2. t线程进行sleep()阻塞时,t的isInterrupted()方法还是返回true,但是sleep()方法如果被提前唤醒,抛出InterruptedException异常,同时会把isInterrupted()方法设为false,此时就要手动决定是否要结束线程了1.6 等待一个线程—join()有时,我们需要等待一个线程完成它的工作后,才能进行自己的下⼀步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。public class Test {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(()->{            for (int i = 0; i < 4; i++) {                System.out.println("hello thread");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        t.start();        System.out.println("main线程开始了");        t.join();        System.out.println("main线程等t线程结束了");    }}1.7 获取当前线程引用1.8 休眠当前线程方法 解释public static native void sleep(long millis) throws InterruptedException; 休眠当前线程 , 以毫米为单位2. 线程的状态线程的状态是一个枚举类型:• NEW: 安排了工作, 还未开始行动• RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作• BLOCKED: 由于加锁产生的阻塞• WAITING: 无超时时间的阻塞• TIMED_WAITING:有超时时间的阻塞• TERMINATED: 工作完成了3. 线程安全3.1 线程不安全案例请大家观察下述代码:public class Test {    private static int count;    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(()->{            for (int i = 0; i < 50_000; i++) {                count++;            }        });        Thread t2 = new Thread(()->{            for (int i = 0; i < 50_000; i++) {                count++;            }        });        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(count);    }}大家认为count最终的值会是100_000吗? 不是的,count最终的值是一个小于100_000的随机数.那为什么呢?线程不安全的概念?如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。3.2 线程不安全的原因线程调度是随机的修改共享数据即多个线程同时修改一个数据原子性什么是原子性?我们把一段代码想象成一个房间,每个线程就是要进入这个房间的⼈。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥,表示操作是互相排斥的。一条 java 语句不一定是原子的,也不一定只是一条指令上述代码中的count++对应着3条指令:load : 从内存把数据读到 CPUadd : 进行数据更新save : 把数据写回到 CPU上述三条指令在多线程中就是有问题的指令.如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。将3种指令执行顺序枚举出我们发现:只有第一种和第二种是正确的内存可见性这里主要个大家介绍一下JMM模型,关于可见性内容请大家查阅目录找volatile关键字Java 内存模型 (JMM—Java Memory Model):Java虚拟机规范中定义了Java内存模型 , 目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.• 线程之间的共享变量存在 主内存 (Main Memory)• 每一个线程都有自己的 “工作内存” (Working Memory)• 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.• 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.因为每个线程有自己的工作内存, 这些工作内存中的内容相当于同⼀个共享变量的 “副本”. 这就导致了此时修改线程1 的工作内存中的值, 线程2 的工作内存不⼀定会及时变化.初始情况下, 两个线程的工作内存内容一致.一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不⼀定能及时同步此时就引入了三个问题:1.为什么要整这么多内存呢?实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法.所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存.CPU的寄存器和缓存统称为工作内存,越往上,速度越快,空间越小,成本越高2.为啥要这么麻烦的拷来拷去?因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是几千倍, 上万倍).比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了.效率就大大提高了.3.那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥??答案就是⼀个字: 贵 , 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度⼜远远快于硬盘.对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜.指令重排序一段代码是这样的:去前台取下 U 盘去教室写 10 分钟作业去前台取下快递如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题的,可以少跑⼀次前台。这种叫做指令重排序.关于指令重排序引发的线程不安全问题请查询目录到单例模式!!!4. synchronized 关键字4.1 synchronized的特性互斥synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象 synchronized 就会阻塞等待.synchronized用的锁是存在Java对象头里的。可以粗略理解成, 每个对象在内存中存储的时候, 都存有⼀块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.如果当前是 “有人” 状态, 那么其他⼈无法使用, 只能排队什么是阻塞等待呢?针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程,再来获取到这个锁.假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁,而是和 C 重新竞争, 并不遵守先来后到的规则.可重入synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题;什么是自己把自己锁死?//第一次加锁,命名为锁1synchronized (locker){//第二次尝试加锁,命名为锁2,但是此时加锁要等到锁1释放锁synchronized (locker){count++;}}//锁1释放锁的条件锁2中的代码要执行完,这就是自己把自己锁死了//理解一下这个场景,车钥匙在家里,家门钥匙在车里但Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.举个例子:加入我追x姑娘,此时x姑娘处于未加锁状态 , 我可以表白成功 , 其他人也可以表白成功 . 但是如果我表白成功了, 意味着x姑娘就处于加锁状态了 , 其他人在想表白是不可能成功的 , 但是我无论想在表白多少次 , x姑娘都会同意在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息:• 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.• 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)4.2 synchronized 使用示例修饰代码块public class SynchronizedDemo {private Object locker = new Object();   public void method() {  synchronized (locker) {   }  }}直接修饰普通方法public class SynchronizedDemo {  public synchronized void methond() {}}修饰静态方法public class SynchronizedDemo {  public synchronized static void method() {}}5. volatile关键字内存可见性import java.util.Scanner; class Counter {    public int flag = 0;}public class Test {    public static void main(String[] args) throws InterruptedException {        Counter count = new Counter();        Thread t1 = new Thread(()->{           while (count.flag == 0){               System.out.println("it is t1 main thread");               try {                   Thread.sleep(1000);               } catch (InterruptedException e) {                   e.printStackTrace();               }           }        });        Scanner scanner = new Scanner(System.in);        Thread t2 = new Thread(()->{            System.out.println("please input a number");            count.flag = scanner.nextInt();        });        t1.start();        t2.start();        t1.join();        t2.join();    }}在这个代码中:• 创建两个线程 t1 和 t2• t1 中包含⼀个循环, 这个循环以 flag == 0 为循环条件.• t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.• 预期当用户输入非 0 的值的时候, t1 线程结束结果发现输入任意一个数字后线程t1并没有停止(这就是一个bug)这是因编译器自身优化导致的bug,当编译器发现我们频繁load的flag是一个值得时候,就会把flag方法工作内存上,就不再上主内存load了,但是我们突然修改flag的值,主内存修改了,但是t1线程的工作内存并没有修改代码在写入 volatile 修饰的变量的时候:• 改变线程工作内存中volatile变量副本的值• 将改变后的副本的值从工作内存刷新到主内存代码在读取 volatile 修饰的变量的时候:• 从主内存中读取volatile变量的最新值到线程的工作内存中• 从工作内存中读取volatile变量的副本前⾯我们讨论JMM模型时说了, 线程直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况. 加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.volatile不能保证原子性虽然volatile解决了内存可见性,但是volatile不是原子的,我们想解决原子性问题还要synchronized锁,volatile和synchronized是两个不同维度的问题6. wait与notify因为线程都是抢占式进行的,并没有固定的顺序,是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.例如一场篮球赛 : 我们要让A球员传球 , B球员拿到球后进行投篮完成这个协调工作, 主要涉及到以下的方法:• wait() / wait(long timeout): 让当前线程进入等待状态.• notify() / notifyAll(): 唤醒在当前对象上等待的线程.注意: wait, notify, notifyAll 都是 Object 类的方法6.1 wait()方法wait 做的事情:• 使当前执行代码的线程进行等待. (把线程放到等待队列中)• 释放当前的锁• 满足一定条件时被唤醒, 重新尝试获取这个锁.注意 : wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.wait 结束等待的条件:• 其他线程调用该对象的 notify 方法.• wait 等待时间超时 (wait 方法提供⼀个带有 timeout 参数的版本, 来指定等待时间).• 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.6.2 notify()方法notify 方法是唤醒等待的线程.• 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。• 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)• 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。6.3 notifyAll()方法notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程notifyAll 一下全都唤醒, 需要这些线程重新竞争锁7. 单例模式首先我们要知道 , 什么是设计模式?设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.单例模式具体的实现方式有很多. 最常见的是 “饿汉” 和 “懒汉” 两种.饿汉模式//类加载的同时创建实例class Singleton{    private Singleton instance = new Singleton();    private Singleton(){};    public Singleton getInstance(){        return instance;    }}懒汉模式—单线程版//类加载的时候不创建实例. 第一次使⽤的时候才创建实例class Singleton{    private static Singleton instence = null;    private Singleton(){};    public static Singleton getInstance(){        if (instence == null){            return new Singleton();        }        return instence;    }}1234567891011懒汉模式—多线程版//使⽤双重 if 判定, 降低锁竞争的频率.//给 instance 加上了 volatile.class Singleton{    private static Object locker = new Object();    private static volatile Singleton instence = null;    private Singleton(){};    public static Singleton getInstance(){                if (instence==null){            synchronized (locker){                if (instence == null){                    return new Singleton();                }            }        }        return instence;    }}理解双重 if 判定:加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了. 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.当多线程首次调⽤ getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作.当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.8. 阻塞队列什么是阻塞队列?阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:• 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.• 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.阻塞队列的⼀个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.8.1 生产者消费者模型生产者消费者模式就是通一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力. (削峰填谷)阻塞队列也能使生产者和消费者之间 解耦.8.2 标准库中的阻塞队列在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.生产者消费者模型:import java.util.Random;import java.util.concurrent.BlockingQueue;import java.util.concurrent.LinkedBlockingQueue;public class Test {    public static void main(String[] args) {        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();        Thread producer = new Thread(()->{            Random random = new Random();           while (true){               try {                   int value = random.nextInt(1000);                   blockingQueue.put(value);                   System.out.println("生产了:"+value);                   Thread.sleep(5000);               } catch (InterruptedException e) {                   e.printStackTrace();               }           }        });        Thread consumer = new Thread(()->{           while (true){               try {                   int value = blockingQueue.take();                   System.out.println("消费了:"+value);                   Thread.sleep(6000);               } catch (InterruptedException e) {                   e.printStackTrace();               }           }        });        producer.start();        consumer.start();    }}8.3 阻塞队列实现• 通过 “循环队列” 的方式来实现.• 使用 synchronized 进行加锁控制.• put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).• take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)public class BlockingQueue {  private int[] items = new int[1000];  private volatile int size = 0;  private volatile int head = 0;private volatile int tail = 0;   public void put(int value) throws InterruptedException {  synchronized (this) {  // 此处最好使⽤ while.  // 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,  // 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能⼜已经队列满了就只能继续等待  while (size == items.length) {  wait();  }  items[tail] = value;  tail = (tail + 1) % items.length;  size++;  notifyAll();}}public int take() throws InterruptedException {int ret = 0;synchronized (this) {while (size == 0) {wait();}ret = items[head];head = (head + 1) % items.length;size--;notifyAll();}return ret;}public synchronized int size() {return size;}}9. 定时器定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.比如⼀个 Map, 希望⾥⾯的某个 key 在 3s 之后过期(自动删除).类似于这样的场景就需要用到定时器.标准库中的定时器:标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule , schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).Timer timer = new Timer();timer.schedule(new TimerTask() {  @Overridepublic void run() {  System.out.println("hello");  }}, 3000);10.线程池虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到⼀个 “池子” 中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.10.1 ExecutorService 和 ExecutorsExecutorService 表示一个线程池实例.Executors 是一个工厂类, 能够创建出几种不同风格的线程池.ExecutorService 的 submit 方法能够向线程池中提交若干个任务. ExecutorService service = Executors.newFixedThreadPool(1);        service.submit(()->{            System.out.println("this is a service");        });Executors 创建线程池的几种方式:newFixedThreadPool: 创建固定线程数的线程池newCachedThreadPool: 创建线程数目动态增长的线程池.newSingleThreadExecutor: 创建只包含单个线程的线程池newScheduledThreadPool: 设定延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.Executors 本质上是 ThreadPoolExecutor 类的封装10.2 ThreadPoolExecutorThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定.把创建一个线程池想象成开个公司. 每个员工相当于一个线程.corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).keepAliveTime: 临时工允许的空闲时间.unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.workQueue: 传递任务的阻塞队列threadFactory: 创建线程的工厂, 参与具体的创建线程⼯作RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.◦ AbortPolicy(): 超过负荷, 直接抛出异常.◦ CallerRunsPolicy(): 调用者负责处理◦ DiscardOldestPolicy(): 丢弃队列中最老的任务.◦ DiscardPolicy(): 丢弃新来的任务.————————————————原文链接:https://blog.csdn.net/2401_82690001/article/details/147980210
  • [技术干货] Java+Selenium+快代理实现高效爬虫
    一、前言在Web爬虫技术中,Selenium作为一款强大的浏览器自动化工具,能够模拟真实用户操作,有效应对JavaScript渲染、Ajax加载等复杂场景。而集成代理服务则能够解决IP限制、地域访问限制等问题。本文将详细介绍如何利用Java+Selenium+快代理实现高效的爬虫系统。二、Selenium简介Selenium是一个用于Web应用程序自动化测试的工具集,它主要用于自动化浏览器操作,可以模拟用户与网页的交互行为,如点击按钮、填写表单、滚动页面等。在爬虫领域,Selenium特别适合处理那些需要JavaScript渲染、需要登录或有反爬措施的网站。三、环境准备JDK1.8Maven项目管理相关依赖<!-- Selenium --><dependency>    <groupId>org.seleniumhq.selenium</groupId>    <artifactId>selenium-java</artifactId>    <version>3.141.59</version></dependency><dependency>    <groupId>io.github.bonigarcia</groupId>    <artifactId>webdrivermanager</artifactId>    <version>5.3.2</version></dependency>四、代码实现本系统采用的是工厂模式创建WebDriver实例,这样做的好处主要是可以提供统一的创建方法,不管使用那种浏览器都适用,自由配置。其次就是维护方便,浏览器配置变更只需修改工厂类中的相关方法,扩展性也不错,可以轻松添加新的浏览器支持,比如Opera或者Safari等等。4.1 创建WebDriver工厂类import io.github.bonigarcia.wdm.WebDriverManager;import org.openqa.selenium.Proxy;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.chrome.ChromeOptions;import org.openqa.selenium.edge.EdgeDriver;import org.openqa.selenium.edge.EdgeOptions;import org.openqa.selenium.firefox.FirefoxDriver;import org.openqa.selenium.firefox.FirefoxOptions;import org.openqa.selenium.remote.CapabilityType;import org.openqa.selenium.remote.PageLoadStrategy;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.concurrent.TimeUnit;/** * WebDriver工厂类,负责创建和配置各种浏览器驱动实例 * 设计思路: * 1. 使用工厂模式统一管理不同浏览器的WebDriver创建逻辑 * 2. 采用构建器模式(Builder Pattern)使配置更加灵活 * 3. 封装复杂的浏览器选项设置,简化调用代码 * 4. 支持多种浏览器类型和代理配置 *  * 好处: * 1. 代码复用性高,减少重复代码 * 2. 配置灵活,通过链式调用设置参数 * 3. 职责单一,仅负责创建WebDriver * 4. 易于扩展,可轻松添加新的浏览器类型支持 */public class WebDriverFactory {    // 使用SLF4J记录日志,便于问题排查    private static final Logger log = LoggerFactory.getLogger(WebDriverFactory.class);        // 默认配置,可通过构建器方法修改    private boolean headless = true;                // 默认无头模式    private int pageLoadTimeoutSeconds = 30;        // 页面加载超时时间    private int scriptTimeoutSeconds = 30;          // 脚本执行超时时间    private int implicitWaitSeconds = 10;           // 隐式等待时间        // 代理配置    private boolean proxyEnabled = false;           // 是否启用代理    private String proxyHost;                       // 代理主机地址    private int proxyPort;                          // 代理端口    private String proxyUsername;                   // 代理用户名(认证用)    private String proxyPassword;                   // 代理密码(认证用)        /**     * 支持的浏览器类型枚举     * 便于扩展,后续可以增加其他浏览器支持     */    public enum BrowserType {        CHROME, EDGE, FIREFOX    }        /**     * 设置是否使用无头模式     * 无头模式下浏览器不会显示界面,更加节省资源     *      * @param headless true表示使用无头模式,false表示显示浏览器界面     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withHeadless(boolean headless) {        this.headless = headless;        return this;    }        /**     * 设置页面加载超时时间     *      * @param seconds 超时秒数     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withPageLoadTimeout(int seconds) {        this.pageLoadTimeoutSeconds = seconds;        return this;    }        /**     * 设置JavaScript脚本执行超时时间     *      * @param seconds 超时秒数     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withScriptTimeout(int seconds) {        this.scriptTimeoutSeconds = seconds;        return this;    }        /**     * 设置元素查找隐式等待时间     *      * @param seconds 等待秒数     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withImplicitWait(int seconds) {        this.implicitWaitSeconds = seconds;        return this;    }        /**     * 配置代理服务器     *      * @param host 代理主机地址     * @param port 代理端口     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withProxy(String host, int port) {        this.proxyEnabled = true;        this.proxyHost = host;        this.proxyPort = port;        return this;    }        /**     * 配置代理服务器认证信息     *      * @param username 代理用户名     * @param password 代理密码     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withProxyAuth(String username, String password) {        this.proxyUsername = username;        this.proxyPassword = password;        return this;    }        /**     * 创建指定类型的WebDriver实例     * 工厂方法核心,根据指定的浏览器类型创建对应的WebDriver     *      * @param browserType 浏览器类型枚举     * @return 配置好的WebDriver实例     */    public WebDriver createWebDriver(BrowserType browserType) {        switch (browserType) {            case CHROME:                return createChromeDriver();            case EDGE:                return createEdgeDriver();            case FIREFOX:                return createFirefoxDriver();            default:                // 默认使用Edge浏览器                log.info("未指定浏览器类型,默认使用Edge浏览器");                return createEdgeDriver();        }    }        /**     * 创建Edge浏览器WebDriver实例     *      * @return 配置好的Edge WebDriver     */    private WebDriver createEdgeDriver() {        // 自动下载与系统浏览器匹配的WebDriver,避免版本不匹配问题        WebDriverManager.edgedriver().setup();                EdgeOptions options = new EdgeOptions();                // 配置浏览器选项        Map<String, Object> edgePrefs = new HashMap<>();        // 禁用自动扩展,减少资源占用和干扰        edgePrefs.put("useAutomationExtension", false);                // 获取通用浏览器参数        List<String> args = getCommonBrowserArgs();                Map<String, Object> edgeOptions = new HashMap<>();        edgeOptions.put("args", args);                // 设置User-Agent,模拟真实浏览器,减少被网站识别为爬虫的可能        options.setCapability("ms.edge.userAgent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0");                // 设置页面加载策略为NORMAL,确保页面完整加载        // 可选值:NONE (不等待加载), EAGER (DOM就绪即可), NORMAL (等待完全加载)        options.setPageLoadStrategy(PageLoadStrategy.NORMAL);                // Edge特有配置        options.setCapability("ms:edgeChromium", true);        options.setCapability("ms:edgeOptions", edgeOptions);        // 使用隐私模式,避免历史记录、cookie等信息的干扰        options.setCapability("inPrivate", true);                // 配置代理        configureProxy(options);                // 创建WebDriver实例        WebDriver driver = new EdgeDriver(options);        // 配置超时设置        configureTimeouts(driver);                log.info("Edge WebDriver创建成功");        return driver;    }        /**     * 创建Chrome浏览器WebDriver实例     *      * @return 配置好的Chrome WebDriver     */    private WebDriver createChromeDriver() {        // 自动下载与系统浏览器匹配的WebDriver        WebDriverManager.chromedriver().setup();                ChromeOptions options = new ChromeOptions();                // 根据配置决定是否使用无头模式        if (headless) {            options.addArguments("--headless");        }                // 添加通用浏览器参数        for (String arg : getCommonBrowserArgs()) {            options.addArguments(arg);        }                // 设置页面加载策略        options.setPageLoadStrategy(PageLoadStrategy.NORMAL);                // Chrome浏览器特殊处理代理配置        configureProxyForChrome(options);                // 创建WebDriver实例        WebDriver driver = new ChromeDriver(options);        // 配置超时设置        configureTimeouts(driver);                log.info("Chrome WebDriver创建成功");        return driver;    }        /**     * 创建Firefox浏览器WebDriver实例     *      * @return 配置好的Firefox WebDriver     */    private WebDriver createFirefoxDriver() {        // 自动下载与系统浏览器匹配的WebDriver        WebDriverManager.firefoxdriver().setup();                FirefoxOptions options = new FirefoxOptions();                // 根据配置决定是否使用无头模式        if (headless) {            options.addArguments("--headless");        }                // 配置代理        configureProxy(options);                // 创建WebDriver实例        WebDriver driver = new FirefoxDriver(options);        // 配置超时设置        configureTimeouts(driver);                log.info("Firefox WebDriver创建成功");        return driver;    }        /**     * 获取通用浏览器启动参数     * 这些参数适用于基于Chromium的浏览器(Chrome, Edge)     *      * @return 参数列表     */    private List<String> getCommonBrowserArgs() {        List<String> args = new ArrayList<>();                // 无头模式相关参数        if (headless) {            args.add("--headless");  // 不显示浏览器界面            args.add("--disable-gpu");  // 在某些系统上无头模式需要禁用GPU加速        }                // 禁用扩展和插件,减少资源占用和干扰        args.add("--disable-extensions");                // 禁用图片加载,提高性能        args.add("--blink-settings=imagesEnabled=false");                // 解决在Docker容器中可能出现的共享内存问题        args.add("--disable-dev-shm-usage");                // 禁用平滑滚动,减少自动滚动问题        args.add("--disable-smooth-scrolling");                // 设置固定窗口大小,避免响应式变化导致的元素定位问题        args.add("--window-size=1366,768");                // 禁用站点隔离,减少内存使用        args.add("--disable-features=site-per-process");                // 禁用默认应用,减少启动时间        args.add("--disable-default-apps");                // 减少日志输出,提高性能        args.add("--disable-logging");                // 禁用信息栏,避免干扰        args.add("--disable-infobars");                // 禁用通知,避免干扰        args.add("--disable-notifications");                // 添加性能优化参数        args.add("--disable-web-security");  // 禁用同源策略检查        args.add("--no-sandbox");  // 禁用沙箱模式,提高性能(注意安全风险)        args.add("--disable-setuid-sandbox");  // 禁用setuid沙箱,配合--no-sandbox使用        args.add("--disable-accelerated-2d-canvas");  // 禁用加速2D Canvas,减少GPU使用        args.add("--disable-crash-reporter");  // 禁用崩溃报告        args.add("--disable-in-process-stack-traces");  // 禁用进程内堆栈跟踪        args.add("--disable-breakpad");  // 禁用断点调试        args.add("--aggressive-cache-discard");  // 积极丢弃缓存,减少内存使用        args.add("--disable-ipc-flooding-protection");  // 禁用IPC洪水保护                // 限制JavaScript引擎内存使用,防止内存溢出        args.add("--js-flags=--max-old-space-size=512");                return args;    }        /**     * 为浏览器选项配置代理     * 适用于Edge和Firefox浏览器     *      * @param options 浏览器选项对象     */    private void configureProxy(Object options) {        if (proxyEnabled && proxyHost != null && !proxyHost.isEmpty() && proxyPort > 0) {            try {                // 构建代理URL,处理是否需要认证                String proxyUrl;                if (proxyUsername != null && !proxyUsername.isEmpty() && proxyPassword != null) {                    // 带认证的代理格式:http://username:password@host:port                    proxyUrl = "http://" + proxyUsername + ":" + proxyPassword + "@" + proxyHost + ":" + proxyPort;                } else {                    // 不带认证的代理格式:http://host:port                    proxyUrl = "http://" + proxyHost + ":" + proxyPort;                }                                // 创建代理对象                Proxy proxy = new Proxy();                // 同时设置HTTP和HTTPS代理,确保所有请求都通过代理                proxy.setHttpProxy(proxyUrl);                proxy.setSslProxy(proxyUrl);                                // 根据浏览器类型设置代理能力                if (options instanceof EdgeOptions) {                    ((EdgeOptions) options).setCapability(CapabilityType.PROXY, proxy);                } else if (options instanceof FirefoxOptions) {                    ((FirefoxOptions) options).setCapability(CapabilityType.PROXY, proxy);                }                                log.info("WebDriver配置了代理: {}", proxyHost + ":" + proxyPort);            } catch (Exception e) {                log.error("配置代理时出错: {}", e.getMessage());            }        }    }        /**     * 为Chrome浏览器特别配置代理     * Chrome处理代理的方式与Edge和Firefox略有不同     *      * @param options Chrome浏览器选项对象     */    private void configureProxyForChrome(ChromeOptions options) {        if (proxyEnabled && proxyHost != null && !proxyHost.isEmpty() && proxyPort > 0) {            try {                // 构建代理URL,处理是否需要认证                String proxyUrl;                if (proxyUsername != null && !proxyUsername.isEmpty() && proxyPassword != null) {                    // 带认证的代理                    proxyUrl = "http://" + proxyUsername + ":" + proxyPassword + "@" + proxyHost + ":" + proxyPort;                } else {                    // 不带认证的代理                    proxyUrl = "http://" + proxyHost + ":" + proxyPort;                }                                // 创建代理对象                Proxy proxy = new Proxy();                proxy.setHttpProxy(proxyUrl);                proxy.setSslProxy(proxyUrl);                                // 为Chrome设置代理能力                options.setCapability(CapabilityType.PROXY, proxy);                                log.info("Chrome WebDriver配置了代理: {}", proxyHost + ":" + proxyPort);            } catch (Exception e) {                log.error("配置Chrome代理时出错: {}", e.getMessage());            }        }    }        /**     * 配置WebDriver的各种超时设置     *      * @param driver WebDriver实例     */    private void configureTimeouts(WebDriver driver) {        // 设置页面加载超时时间        driver.manage().timeouts().pageLoadTimeout(pageLoadTimeoutSeconds, TimeUnit.SECONDS);        // 设置脚本执行超时时间        driver.manage().timeouts().setScriptTimeout(scriptTimeoutSeconds, TimeUnit.SECONDS);        // 设置隐式等待时间,查找元素时使用        driver.manage().timeouts().implicitlyWait(implicitWaitSeconds, TimeUnit.SECONDS);                log.debug("WebDriver超时配置完成:页面加载={}秒,脚本执行={}秒,隐式等待={}秒",                pageLoadTimeoutSeconds, scriptTimeoutSeconds, implicitWaitSeconds);    }}4.2 创建爬虫主类import org.openqa.selenium.By;import org.openqa.selenium.WebDriver;import org.openqa.selenium.WebElement;import org.openqa.selenium.support.ui.ExpectedConditions;import org.openqa.selenium.support.ui.WebDriverWait;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.List;/** * Selenium爬虫示例主类 * 演示如何使用WebDriverFactory创建浏览器实例并进行网页爬取 */public class SeleniumCrawler {    private static final Logger log = LoggerFactory.getLogger(SeleniumCrawler.class);        public static void main(String[] args) {        // 推荐使用快代理的隧道代理:https://www.kuaidaili.com/?ref=soi1rkc6rd82        String proxyHost = "";  // 快代理隧道代理主机        int proxyPort = 15818;                // 端口,根据实际情况修改        String proxyUsername = "yourUsername"; // 替换为您的快代理用户名        String proxyPassword = "yourPassword"; // 替换为您的快代理密码                // 创建WebDriver工厂实例,配置爬虫参数        // 使用构建器模式,代码可读性强,配置灵活        WebDriverFactory factory = new WebDriverFactory()            .withHeadless(false)  // 设置为false可以看到浏览器界面,方便调试            .withPageLoadTimeout(30)  // 页面加载超时设置为30秒            .withScriptTimeout(30)    // 脚本执行超时设置为30秒              .withImplicitWait(10)     // 查找元素隐式等待10秒            .withProxy(proxyHost, proxyPort)           // 设置快代理的主机和端口            .withProxyAuth(proxyUsername, proxyPassword);  // 设置代理认证信息                WebDriver driver = null;        try {            // 创建Edge浏览器实例,也可以选择Chrome或Firefox            log.info("正在初始化WebDriver...");            driver = factory.createWebDriver(WebDriverFactory.BrowserType.EDGE);                        // 开始爬虫任务            crawlWebsite(driver);                    } catch (Exception e) {            // 异常处理,记录详细错误信息便于排错            log.error("爬虫执行出错: {}", e.getMessage(), e);        } finally {            // 确保WebDriver正确关闭,避免资源泄露            if (driver != null) {                driver.quit();                log.info("WebDriver已关闭,爬虫任务结束");            }        }    }        /**     * 爬虫核心逻辑,可根据实际需求扩展     *      * @param driver 已配置好的WebDriver实例     * @throws InterruptedException 如果线程休眠被中断     */    private static void crawlWebsite(WebDriver driver) throws InterruptedException {        // 访问目标网站        log.info("开始访问目标网站");        driver.get("https://www.baidu.com");        log.info("网页标题: {}", driver.getTitle());                // 显式等待某个元素出现,确保页面加载完成        // 比简单的Thread.sleep更智能        WebDriverWait wait = new WebDriverWait(driver, 10);        wait.until(ExpectedConditions.presenceOfElementLocated(By.tagName("body")));                // 获取页面内容示例:提取所有链接        log.info("开始提取页面链接");        List<WebElement> links = driver.findElements(By.tagName("a"));        log.info("共发现{}个链接", links.size());                // 处理提取到的链接        for (WebElement link : links) {            String text = link.getText().trim();            String href = link.getAttribute("href");            // 只记录有效链接            if (href != null && !href.isEmpty()) {                log.info("链接: {} -> {}", text.isEmpty() ? "[无文本]" : text, href);            }        }                // 模拟更多爬虫操作,例如点击某个元素、填写表单等        // 这里作为示例,只是简单等待        log.info("等待页面进一步处理...");        Thread.sleep(2000);                // 如果需要,可以继续访问更多页面        // driver.get("https://www.another-site.com");        // ...                log.info("爬虫任务完成");    }}4.3 配置代理的注意事项在使用代理时,需要注意以下几点:选择合适的代理类型:隧道代理适合大规模爬虫,普通代理适合小规模测试正确配置认证信息:确保用户名和密码正确,特殊字符需要URL编码测试代理连通性:使用前先测试代理是否可用合理设置请求频率:遵循代理服务商的使用建议,避免触发反爬机制注意IP切换时机:适时切换IP,避免同一IP频繁访问目标网站六、总结与展望本文详细介绍了如何使用Java+Selenium+快代理实现高效的网页爬虫。通过工厂模式和构建器模式的应用,我们实现了一个灵活、可扩展且易于使用的爬虫框架。该框架解决了代理认证配置的难题,优化了浏览器参数设置,提高了爬虫的稳定性和效率。Selenium与代理服务的结合为我们提供了强大的爬虫能力:Selenium模拟真实用户行为应对JavaScript渲染和复杂交互,而快代理则提供了稳定的IP资源池,有效规避IP封禁和地域限制问题。这种组合特别适合需要处理登录验证、动态加载内容或有反爬措施的网站。在实际应用中,请务必遵守相关法律法规和网站的使用条款,合理设置爬虫的请求频率和数量,避免对目标网站造成不必要的负担。同时,定期更新Selenium和WebDriver版本,以适应浏览器的更新和网站的变化。如果你在使用过程中遇到问题,可以参考快代理或查阅Selenium的相关资料。希望本文对你的爬虫开发有所帮助!最后,随着网站反爬技术的不断进化,爬虫技术也需要持续更新迭代。未来,我们可以考虑结合机器学习技术识别验证码,或通过更智能的策略调整爬取行为,使爬虫更加智能和高效。欢迎在评论区分享你的使用经验和改进建议————————————————原文链接:https://blog.csdn.net/weixin_66401877/article/details/147825058
总条数:739 到第
上滑加载中