2023.10.22 关于 定时器(Timer) 详解

news2024/12/23 0:54:25

目录

引言

标准库定时器使用

自己实现定时器的代码

模拟实现的两大方面

核心思路

重点理解

 自己实现的定时器代码最终代码版本


引言

  • 定时器用于在 预定的时间间隔之后 执行特定的任务或操作

实例理解:

  • 在服务器开发中,客户端向服务器发送请求,等待服务器响应,但可能因为某一故障,导致程序一直无法响应,从而容易出现客户端卡死的情况,所以为了应对该情况,我们通常可以设置一个定时器,若未在规定的时间内完成任务,则可以做一些操作,来取消客户端的等待

标准库定时器使用

import java.util.Timer;
import java.util.TimerTask;

public class ThreadDemo24 {
    public static void main(String[] args) {
        System.out.println("程序启动");
//        这个 Timer 类就是标准库的定时器
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("运行定时器任务A");
            }
        },3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("运行定时器任务B");
            }
        },4000);
    }
}
  • 上述代码中的 schedule 方法的效果是给定时器 注册一个任务,任务不会立即执行,而是在指定时间进行执行
  • 上述代码中的 schedule 方法有两个参数,一个参数为 TimerTask 接口,对 run 方法进行重写,从而指定要执行的任务,另一个参数为等待时间且单位为毫秒

注意:

  • 一个定时器可以同时安排多个任务
  • 定时器执行完任务之后,进程并不会立即退出,因为定时器内部需要维护一组线程来执行这些任务,这些线程被称为 前台线程
  • 当我创建一个定时器并安排任务时,定时器会启动一个或多个线程,这些线程负责按计划执行任务
  • 这些线程会一直运行,直到定时器被取消或程序显式地终止,这样做到目的是为了确保定时器能够准时执行任务,即使主线程已经完成或已退出
  • 由于这些前台线程在定时器内部运行,所以它们会影响进程的退出
  • 如果定时器中的任务尚未完成,这些前台线程将阻止进程退出,直到所有任务执行完毕或定时器被取消
  • 这确保了任务得到完整执行,并且程序能够正常结束
  • 从而需要注意的是,在使用定时器时不再需要它,我们应该主动取消定时器以释放资源并停止前台线程的执行
  • 这样可以避免不必要的资源占用和线程执行

自己实现定时器的代码

模拟实现的两大方面

  • 在指定时间执行所注册的任务
  • 一个定时器可注册多个任务,且这多个任务按照约定时间,顺序执行

核心思路

  • 有一个扫描线程,负责判定任务是否到执行时间
  • 需要一个 数据结构 来保存所有被注册的任务

注意:

  • 此处的每个任务均带有时间,并且一定是时间越靠前,就执行
  • 所以在当下的场景中使用 优先级队列 便是一个很好的选择
  • 时间小的,作为优先级高的
  • 此时队首元素 就是整个队列中 最先要执行的任务
  • 此时 扫描线程仅需扫描一下队首元素即可,不必遍历整个队列
  • 因为队首元素还没到执行时间,后续元素更不可能到执行时间
  • 当然 此处的优先级队列会在 多线程 环境下使用
  • 因为 调用 schedule 方法是一个线程,扫描是另一个线程,从而此处涉及到线程安全问题
  • 我们可以使用 标准库提供的 PriorityBlockingQueue ,阻塞队列本身就是线程安全的,所以 带优先级的阻塞队列 便十分切合我们的需求

  • 以下是一个自己实现的定时器
import java.util.concurrent.PriorityBlockingQueue;

class MyTask implements Comparable<MyTask>{
//    要执行的任务内容
    private Runnable runnable;
//    任务在啥时候执行(使用毫秒时间戳表示)
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }
//    获取当前任务的时间
    public long getTime() {
        return time;
    }
//    执行任务时间
    public void  run() {
        runnable.run();
    }

    @Override
    public int compareTo(MyTask o) {
//        当前想要实现队首元素是 时间最小的任务
//        这两是 谁减谁,不需要去记,往往可以试一试就知道了
//        要么就是 this.time - o.time, 要么就是 o.time - this.time
        return (int) (this.time - o.time);
    }
}

class MyTimer {
//    扫描线程
    private Thread t = null;

    public MyTimer() {
        t = new Thread(() -> {
           while (true) {
//               取出队首元素,检查看看队首元素任务是否到时间了
               try {
                   synchronized (this) {
                       MyTask myTask = queue.take();
                       long curTime = System.currentTimeMillis();
                       if(curTime < myTask.getTime()) {
//                         如果时间还没到,就把任务塞回队列
                           queue.put(myTask);
//                           在 put 之后,进行一个 wait 等待
                           this.wait(myTask.getTime() - curTime);
                       }else {
//                           如果时间到了,就把任务进行执行
                           myTask.run();
                       }
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }

//    有一个阻塞优先级队列,来保存任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
//    指定两个参数
//    第一个参数是 任务 内容
//    第二个参数是 任务 在多少毫秒之后执行 如 1000
    public void schedule(Runnable runnable,long after) {
//        注意这里的时间换算,获取当前时间的时间戳加上需要等待的时间就是任务执行的时间
        MyTask task = new MyTask(runnable,System.currentTimeMillis() + after);
        queue.put(task);
        synchronized (this) {
            this.notify();
        }
    }
}

public class ThreadDemo25 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行了任务A");
            }
        },1000);

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行了任务B");
            }
        },2000);
    }
}

运行结果:


重点理解

  • MyTask 类是用来描述一个任务的
  • 其中包含 要执行的任务内容 和 任务在什么时候执行

  • 正因为我们使用 优先级阻塞队列 来保存我们所有被注册的任务
  • 所以我们需要指定当前任务的优先级是什么
  • 此处我们任务的优先级是 越早执行的任务其优先级越高,即队首元素是时间最小的任务
  • 从而我们需要实现一个 Comparable 接口,并重写 compareTo 方法  

注意:

  • 此处的变量 time 为 long 类型,所以需要强制类型转换为 int 类型
  • 而且此处到底是(this.time - o.time)还是(o.time - this.time),我们仅试试就知道了,毕竟就这两种减法,不要去死记硬背

  • 关于这里的时间戳

  • 在 Java 中,System.currentTimeMillis() 是一个静态方法,它返回自1970年1月1日午夜(格林威治时间)以来当前时间的毫秒数
  • 即返回一个 long类型的值,表示当前时间与1970年1月1日午夜之间的毫秒数差,这个值可以用来计算时间间隔、时间戳等操作
  • 类似于输出 1626379152345

  • 这里我们为什么要引入 wait 和 notify 呢?

  • queue.put 操作是从 queue 中取出首元素
  • 此时的 queue.put 操作是放在 while 循环中的,因为我们想保证任务能够及时执行,所以不断的循环取出我们 queue 中的首元素,拿出来与当前的时间进行比较,以免错过任务的执行时间
  • 但是我们会发现,当距离队首元素执行任务还有很长一段时间的时候,queue 也会快速循环地将队首元素取出、比较、放回,那此时很显然就是在 忙等,CPU 不停地执行该循环操作,却毫无意义
  • 那么我们便可以直接引入wait 和 notify 来解决此情形
  • 当 queue 取出队首元素进行比较时,如果发现其还未到执行时间,那么将再次把该元素放回到 queue 中,然后再 wait 阻塞等待当前时间与首元素执行时间的时间差
  • 正因为在 wait 阻塞等待的期间中,可能还会插入新元素,并且不能保证该新元素是否会成为 queue 中新的首元素,所以在我们每插入一个新元素时,都需要进行 notify 一次,唤醒线程,然后继续执行 while 循环中的操作
  • 所以此处不能简单的使用 sleep 进行阻塞等待,因为无法感知新元素插入所导致的新改变

  • 使用 wait 的前提是得拥有锁对象,所以要进行加锁操作
  • 那么此时的 synchronized 有以下两种加锁方式,哪种更好呢?

  • 我们拿 方案二的加锁方式 进行分析
  • 当 线程t 执行 queue.take 语句时,此时便会将 queue 的队首元素取出来,然后准备进行比较操作
  • 假设队首元素的执行时间为 11点,且此时的时间为 9点,即该队首元素还未到执行时间,那么便将会把该元素重新放回到 queue 中
  • 如果此时的 线程t 正准备要执行 wait 进行阻塞等待时,CPU 转而执行其他,也就是在还未 wait 的情况下,又新增了一个任务,并且此时该任务的执行时间为 10点
  • 那么在新增任务的前提下,继续执行 wait 阻塞等待 2个小时 ,此时便会直接错过准备在10点 执行的新任务
  • 造成上述情况最主要的原因就是方案二并没有保证 take 和 wait 这两个操作执行的原子性,导致在执行这两个操作之间,可能会 put 进一个新任务
  • 所以我们可以将 synchronized 加锁的范围扩大,直接将 锁的范围扩大到 方案一,以此想保证 take 和 wait 操作的原子性

  • 但是仅这样我们能解决上述问题嘛?
  • 也就是能否保证在执行 take 和 wait 这两个操作时,执行这两个操作之间,不会再 put 进一个新任务
  • 显然仅通过扩大上述 synchronized 加锁的范围扩大,并不能完全保证,我们得保证再对锁对象加锁时,其 put 方法也需放入到 同一个锁对象的锁中,即将使用方案二

  • 也就是当 扫描线程t 对 锁对象进行了加锁操作,此时其他线程便不能调用被  同一个 锁对象 加了锁的代码块
  • 具体来说就是当主线程调用 schedule 方法准备执行 queue.put 语句插入新任务时, 便因为 扫描线程t 未释放 锁对象,所以主线程不能获取到锁对象,从而便会阻塞在 锁外,从而只要当 扫描线程t 释放了锁对象,主线程才能获取到锁对象,也就才能执行 queue.put 语句,才能往 queue 队列中插入新任务
  • 所以通过以上修改,我们便能很好的保证在执行 take 和 wait 这两个操作时,执行这两个操作之间,不会再 put 进一个新任务

  • 仔细思考上述代码,我们还会发现问题
  • 那就是通过  synchronized 加锁的范围扩大 和 把 put 方法也放入到 同一个锁对象的锁中 这两个操作,虽然解决了出现下图所示情况

  • 但是经过上述调整,该代码会存在 死锁 的情况
  • 假设此时 new 了一个 MyTimer 对象定时器
  • 那么此时就会初始化并调用 MyTimer 构造方法,构造方法就会创建一个线程t1,并开始执行其 run 方法,此时 线程t 便会拿到锁对象,程序进入 run 方法,但是由于 queue 队列中没有元素,因此就会在 queue.take 处阻塞等待,直到有任务放入队列中
  • 此时主线程通过调用 schedule 方法准备往 queue 队列中加入任务,但是由于 线程t 已经拿到锁对象了,且并未释放锁对象,所以此时在准备执行 queue.put 语句时,便会阻塞等待,所以此时 schedule 无法将任务 put 到 queue 队列中,这时 线程t 在阻塞等待,schedule 也在阻塞等待,就出现了死锁

模拟代码示例

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class Syn {

    private Object locker = new Object();
    BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();

    public Syn() {
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                try {
                    queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        });
        t1.start();
    }

    public void say() throws InterruptedException {
        synchronized (locker) {
            queue.put(Integer.valueOf("1"));
        }
    }
}

public class TestSyn {
    public static void main(String[] args) throws InterruptedException {
        Syn syn = new Syn();
        Thread.sleep(1);
        syn.say();
        System.out.println("主线程打印 queue 中的值:"+ syn.queue.take());
    }
}

执行结果:

  • 我们发现新插入到 queue 中的值并未打印到控制台

通过 jconsole 观察线程情况:

  • 综上所述,为了 防止死锁的发生,我们又需将 queue.put 操作放到 锁外
  • 与上文通过  synchronized 加锁的范围扩大 和 把 queue.put 操作也放入到 同一个锁对象的锁中 这两个操作来保证 take 和 wait 这两个操作执行的原子性,也就是在执行 take 和 wait 这两个操作之间,不会再 put 进一个新任务
  • 从而这里 queue.take 无论是放在锁外还是锁内,都会引发问题

 自己实现的定时器代码最终代码版本

  • 使用 优先级阻塞队列 无论如何修改代码总会存在 问题,所以 我们直接转而使用 优先普通级队列
  • 不再使用 自带阻塞效果的 take 和 put 方法了
import java.util.PriorityQueue;

// 创建一个类, 用来描述定时器中的一个任务
class MyTimerTask implements Comparable<MyTimerTask> {
    // 任务啥时候执行. 毫秒级的时间戳.
    private long time;
    // 任务具体是啥.
    private Runnable runnable;

    public MyTimerTask(Runnable runnable, long delay) {
        // delay 是一个相对的时间差. 形如 3000 这样的数值.
        // 构造 time 要根据当前系统时间和 delay 进行构造.
        time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        // 认为时间小的, 优先级高. 最终时间最小的元素, 就会放到队首.
        // 怎么记忆, 这里是谁减去谁?? 不要记!! 记容易记错~~
        // 随便写一个顺序, 然后实验一下就行了.
        return (int) (this.time - o.time);
        // return (int) (o.time - this.time);
    }
}

// 定时器类的本体
class MyNewTimer {
    // 使用优先级队列, 来保存上述的 N 个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    // 用来加锁的对象
    private Object locker = new Object();

    // 定时器的核心方法, 就是把要执行的任务添加到队列中.
    public void schedule(Runnable runnable, long delay) {
        synchronized (locker) {
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            // 每次来新的任务, 都唤醒一下之前的扫描线程. 好让扫描线程根据最新的任务情况, 重新规划等待时间.
            locker.notify();
        }
    }

    // MyTimer 中还需要构造一个 "扫描线程", 一方面去负责监控队首元素是否到点了, 是否应该执行; 一方面当任务到点之后,
    // 就要调用这里的 Runnable 的 Run 方法来完成任务
    public MyNewTimer() {
        // 扫描线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            // 注意, 当前如果队列为空, 此时就不应该去取这里的元素.
                            // 此处使用 wait 等待更合适. 如果使用 continue, 就会使这个线程 while 循环运行的飞快,
                            // 也会陷入一个高频占用 cpu 的状态(忙等).
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            // 假设当前时间是 14:01, 任务时间是 14:00, 此时就意味着应该要执行这个任务了.
                            // 需要执行任务.
                            queue.poll();
                            task.getRunnable().run();
                        } else {
                            // 让当前扫描线程休眠一下, 按照时间差来进行休眠.
                            // Thread.sleep(task.getTime() - curTime);
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

// 写一个定时器
public class ThreadDemo27 {
    public static void main(String[] args) {
        MyNewTimer timer = new MyNewTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3");
            }
        }, 3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2");
            }
        }, 2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1");
            }
        }, 1000);
        System.out.println("程序开始运行");
    }
}

原因如下图所示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1127377.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Banana Pi BPI-W3(Armsom W3)RK3588开当板之调试UART

前言 本文主要讲解如何关于RK3588开发板UART的使用和调试方法&#xff0c;包括UART作为普通串口和控制台两种不同使用场景 一. 功能特点 Rockchip UART (Universal Asynchronous Receiver/Transmitter) 基于16550A串口标准&#xff0c;完整模块支持以下功能&#xff1a; 支…

【T+】畅捷通T+增加会计科目提示执行超时已过期。

【问题描述】 在畅捷通T软件中&#xff0c; 增加会计科目的时候提示&#xff1a; 通过DataTable插入ext扩展表出错:执行超时已过期。 完成操作之前已超时或服务器未响应。 操作已被用户取消。 语句已终止。 【解决方法】 【方法一】 注销用户登录&#xff0c;回到软件登录界面…

linux入门---多线程的理解

目录标题 线程的认识线程的管理进程和线程的区别为什么要有多线程线程的特性多线程的创建和证明线程特性的补充线程的优点线程的缺点线程的健壮性问题clone函数 线程的认识 在之前的学习中我们知道当一个程序加载进物理内存的时候操作系统会为该程序创建对应的PCB&#xff0c;…

11 Fork/Join

1 分治思想 分治思想&#xff1a;规模为N的问题分解为K个规模的子问题&#xff0c;子问题相互独立且与原问题性质相同&#xff0c;求出子问题的解&#xff0c;就能得到原问题的解 分治思想的步骤&#xff1a; 分解 求解 合并 2 Fork/Join 2.1 介绍 并行计算框架&#xff0c;用…

ROCESS SPID 代表什么进程

ROCESS 发出sql命令 所在主机的进程 可以不在数据库主机上发出 SPID 对应数据库的服务进程id select a.PROCESS,b.SPID From v$session a , v$process b where a.PADDRb.ADDR and a.USERNAMESYS SQL> !ps -ef|grep sqlplus oracle 385 2792 0 21:01 pts/…

继电器测试的方法和步骤有哪些?

继电器测试是一种常见的电气测试方法&#xff0c;用于检测继电器的工作状态和性能&#xff0c;下面是继电器测试的一般方法和步骤&#xff1a; 准备工作&#xff1a;确认测试设备的准备情况&#xff0c;包括测试仪器、电源、继电器和连接线等。确认继电器的工作原理和参数&…

IDEA: 个人主题及字体搭配推荐

文章目录 1. 字体设置推荐2. 主题推荐3. Rainbow Brackets(彩虹括号)4. 设置背景图片 下面是我的 IDEA 主题和字体&#xff0c;它们的搭配效果如下&#xff1a; 1. 字体设置推荐 在使用 IntelliJ IDEA 进行编码和开发时&#xff0c;一个合适的字体设置可以提高你的工作效率和舒…

Spring 最全Bean的加载和获取方式整理

目录 一、 前言二、Bean加载的九种方式1. XML配置方式2. XML注解配置方式3. 注解方式4. 使用Bean方式5. 使用Import方式6. 容器初始化完毕后注入bean7. 实现ImportSelector接口8. 实现ImportBeanDefinitionRegistrar接口9. 实现BeanDefinitionRegistryPostProcessor接口 三、Be…

Verilog功能模块——读写位宽不同的异步FIFO

前言 前面的博文已经讲了异步FIFO和同步FIFO&#xff0c;但并没有实现FIFO的读写位宽转换功能&#xff0c;此功能是FIFO的主要功能之一&#xff0c;应用十分广泛&#xff0c;因此&#xff0c;在前面两个模块的基础上&#xff0c;本文使用纯Verilog实现了读写位宽不同的FIFO&…

Plex踩坑——plex web无法找到媒体服务器

现象&#xff1a;之前安装过plex server&#xff0c;然后卸载了。再次重装后&#xff0c;plex web无法找到媒体服务器。 原因&#xff1a;卸载plex server时需要手动将plex的注册表删除&#xff1a;HKEY_CURRENT_USER\Software\Plex, Inc. 原文链接Uninstall Plex Media Serve…

又是一年1024,你还在做程序猿嘛

每年的10月24日&#xff0c;对于广大程序员来说&#xff0c;都有着特殊的意义。这一天是程序员节&#xff0c;一个属于这个独特群体的庆祝活动。在这个特别的日子里&#xff0c;我们不禁要问&#xff1a;又是一年1024&#xff0c;你还在做程序猿嘛&#xff1f; 程序员&#xff…

MATLAB——一维连续小波的分解

%% 学习目标&#xff1a;一维连续小波的分解 %% help wavelet 查询小波工具箱中的所有函数 %% wavedemo 查看案例 clear all; close all; load noissin.mat; %% which noissin.mat figure; subplot(211); plot(noissin); %信号的时域图 10…

【xxl-job】你与xxl-job仅差这个示例

文章目录 摘要介绍底层使用技术和实现原理分布式任务调度任务执行器分片任务任务调度中心 示例代码详解创建一个任务处理类补充配置文件启动xxl-job执行器&#xff0c;并在任务调度中心中添加一个定时任务在任务调度中心中添加一个定时任务&#xff0c;并选择刚刚创建的任务处理…

只要路由器有WPS按钮,佳能打印机连接到Wi-Fi网络的方法就很简单

佳能打印机是很好的设备&#xff0c;可以让你从智能手机、电脑或平板电脑打印照片。它们还提供其他功能&#xff0c;如扫描文档和复制图像。 最新的型号还允许你连接到Wi-Fi&#xff0c;因此你不需要使用电线将设备连接到打印机。 Wi-Fi是通过本地网络传输数据的标准方式。它…

rstudio server 服务器卡死了怎么办

#rstudio 卡死了怎么办 cd ~/.local/share/ ls rm -fr rstudio.old mv ~/.rstudio ~/.rstudio.oldcd ~/.config/ rm -fr .rstudio.old mv ~/.config/rstudio/ ~/.config/rstudio.oldps -ef|grep t040413 |grep rsession |awk {print $2}| xargs kill -9

itbuilder软件在线设计数据库模型,AI与数据库擦出的火花

今天要介绍一款强大的软件&#xff0c;它就是itBuilder软件&#xff0c;一款在线设计数据库模型软件&#xff0c;借助人工智能提高效率&#xff0c;可以生成CRUD代码并推送至开发工具中&#xff1b;它涵盖了几乎所有语言&#xff0c;如Java、Python、JavaScript等&#xff0c;并…

基于Python开源爬虫框架Scrapy租房信息爬取与数据展示工具

获取代码&#xff1a; 知识付费时代&#xff0c;低价有偿获取代码&#xff0c;请理解&#xff01; (1) 下载链接: 后发 (2) 添加博主微信获取&#xff08;有偿&#xff09;,备注来源: mryang511688 (3) 快速扫码咨询&#xff1a; 项目描述 技术&#xff1a;Python、Scrapy、Dj…

红海云签约深圳天使母基金,数智引领金融行业人力资源数字化转型

深圳市天使投资引导基金管理有限公司&#xff08;以下简称“深圳天使母基金”&#xff09;是深圳市人民政府投资发起设立的战略性、政策性基金&#xff0c;目前规模100亿元&#xff0c;是国内规模最大的天使投资类政府引导基金&#xff0c;致力于成为全球领先的天使母基金。 近…

youyeetoo R1卡片电脑(rk3588s)

简介&#xff1a; youyeetoo R1 是风火轮科技专为AIOT市场设计的嵌入式主板(SBC)&#xff0c;体积小但功能强大&#xff0c;搭载瑞芯微旗舰级RK3588s 八核64位处理器&#xff0c;8nm 制程&#xff0c;主频高达2.4GHz&#xff0c;集成ARM Mali-G610 MP4 GPU&#xff0c;内置6 To…

【原创】解决Kotlin无法使用@Slf4j注解的问题

前言 主要还是辟谣之前的网上的用法&#xff0c;当然也会给出最终的使用方法。这可是Kotlin&#xff0c;关Slf4j何事&#xff01;&#xff1f; 辟谣内容&#xff1a;创建注解来解决这个问题 例如&#xff1a; Target(AnnotationTarget.CLASS) Retention(AnnotationRetentio…