通过 Java Timer/TimerTask 源码原理解析告诉你如何实现定时任务

《Java源码分析》:Timer/TimerTask 《Java源码分析》:Timer在JDK 5.0之前,java.util.Timer/TimerTask是唯一的内置任务调度方法,而且在很长一段时间里很热衷于使用这种方式进行周期性任务调度。本篇博文就先使用Timer/TimerTask来完成任务的调度。接着再来分析Timer/TimerTask的源码。Timer类中常见方法1、void cancel() 终止此计时器,丢弃所 阅读详情

如何实现定时任务- Java Timer/TimerTask 源码原理解析

一、背景和使用

    日常实现各种服务端系统时,我们一定会有一些定时任务的需求。比如会议提前半小时自动提醒,异步任务定时/周期执行等。那么如何去实现这样的一个定时任务系统呢? Java JDK提供的Timer类就是一个很好的工具,通过简单的API调用,我们就可以实现定时任务。

    现在就来看一下java.util.Timer是如何实现这样的定时功能的。

    首先,我们来看一下一个使用demo

Timer timer = new Timer(); 
TimerTask task = new TimerTask() { 
    public void run() { 
        System.out.println("executing now!"); 
    } 
};

// 延迟 1s 打印一次 
timer.schedule(task, 1000) 
// 延迟 1s 固定时延每隔 1s 周期打印一次
timer.schedule(task, 1000, 1000); 
// 延迟 1s 固定速率每隔 1s 周期打印一次
timer.scheduleAtFixRate(task, 1000, 1000)
复制代码

基本的使用方法:

  1. 创建一个Timer对象
  2. 创建一个TimerTask对象,在这里实现run方法
  3. 将TimerTask对象作为参数,传入到Timer对象的scheule方法中,进行调度执行。

加入任务的API如下:

  • 一次性任务的API
   // 指定时延后运行
   // 默认fixed-delay模式,周期时间按上一次执行结束时间计算
   public void schedule(TimerTask task, long delay) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        sched(task, System.currentTimeMillis()+delay, 0);
    }
    
    // 指定时间点运行
    public void schedule(TimerTask task, Date time) {
        sched(task, time.getTime(), 0);
    }
复制代码
  • 周期任务的APi:
    // 指定时延后运行,之后以指定周期运行
    public void schedule(TimerTask task, long delay, long period) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, System.currentTimeMillis()+delay, -period);
    }

    // 指定时间点运行,之后以指定周期运行
    public void schedule(TimerTask task, Date firstTime, long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), -period);
    }

    // 指定时延后运行,之后以指定周期运行
    // 默认fixedRate模式,周期时间按任务执行开始时间计算
    public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, System.currentTimeMillis()+delay, period);
    }

    public void scheduleAtFixedRate(TimerTask task, Date firstTime,
                                    long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), period);
    }
复制代码

可以看到API方法内部都是调用sched方法,其中time参数下一次任务执行时间点,是通过计算得到。period参数为0的话则表示为一次性任务。

二、内部原理

那么我们来看一下Timer内部是如何实现调度的。

内部结构

先看一下Timer的组成部分:

public class Timer {

    // 任务队列
    private final TaskQueue queue = new TaskQueue();

    // 工作线程,循环取任务
    private final TimerThread thread = new TimerThread(queue);

    private final Object threadReaper = new Object() {
        protected void finalize() throws Throwable {
            synchronized(queue) {
                thread.newTasksMayBeScheduled = false;
                queue.notify(); // In case queue is empty.
            }
        }
    };

    // Timer的序列号,命名工作线程(静态变量,在启动多个Timer的情况可以用于区分对应的工作线程)
    private final static AtomicInteger nextSerialNumber = new AtomicInteger(0);  
}

复制代码

Timer有3个重要的模块,分别是 TimerTask, TaskQueue, TimerThread

  • TimerTask,即待执行的任务
  • TaskQueue,任务队列,TimerTask加入后会按执行时间自动排序
  • TimerThread,工作线程,真正循环执行TimerTask的线程

那么,在加入任务之后,整个Timer是怎么样运行的呢?可以看下面的示意图:

Timer模块结构

图中所示是简化的逻辑,多个任务加入到TaskQueue中,会自动排序,队首任务一定是当前执行时间最早的任务。TimerThread会有一个一直执行的循环,从TaskQueue取队首任务,判断当前时间是否已经到了任务执行时间点,如果是则执行任务。

工作线程

  1. 创建任务,调用scheule方法
public void schedule(TimerTask task, Date firstTime, long period) { 
    if (period <= 0) 
        throw new IllegalArgumentException("Non-positive period."); 
    sched(task, firstTime.getTime(), -period); 
}
复制代码
  1. 内部调用sched方法
// sched方法的入参是task任务,执行的时间,以及执行周期
private void sched(TimerTask task, long time, long period) {
    if (time < 0)
        throw new IllegalArgumentException("Illegal execution time.");

    // 防止溢出
    if (Math.abs(period) > (Long.MAX_VALUE >> 1))
        period >>= 1;

    // 对queue加锁,避免并发入队
    synchronized(queue) {
        if (!thread.newTasksMayBeScheduled)
            throw new IllegalStateException("Timer already cancelled.");
        
        // 对task加锁,避免并发修改
        synchronized(task.lock) {
            if (task.state != TimerTask.VIRGIN)
                throw new IllegalStateException(
                    "Task already scheduled or cancelled");
            task.nextExecutionTime = time;
            task.period = period;
            task.state = TimerTask.SCHEDULED;
        }
        // 任务入队
        queue.add(task);
        /* 如果任务是队列当前第一个任务,则唤醒工作线程
           这里是因为工作线程处理完上一个任务之后,会 sleep 到下一个 task 的执行时间点。
           如果有 nextExecutionTime 更早的 task 插队到前面,则需要马上唤醒工作线程进行检查
           避免 task 延迟执行
        */
        if (queue.getMin() == task)
            queue.notify();
    }
}

复制代码

流程中加了一些锁,用来避免同时加入TimerTask的并发问题。可以看到sched方法的逻辑比较简单,task赋值之后入队,队列会自动按照nextExecutionTime排序(升序,排序的实现原理后面会提到)。

  1. 工作线程的mainLoop
public void run() {
    try {
        mainLoop();
    } finally {
        synchronized(queue) {
            newTasksMayBeScheduled = false;
            queue.clear();
        }
    }
}

/**
 * 工作线程主逻辑,循环执行
 */
private void mainLoop() {
    while (true) {
        try {
            TimerTask task;
            boolean taskFired; // 标记任务是否应该执行
            synchronized(queue) {
                // 如果队列为空,且newTasksMayBeScheduled为true,此时等待任务加入
                while (queue.isEmpty() && newTasksMayBeScheduled)
                    queue.wait();
                
                // 如果队列为空,且newTasksMayBeScheduled为false,说明此时线程应该退出
                if (queue.isEmpty())
                    break;

                // 队列不为空,尝试从队列中取task(目标执行时间最早的task)
                long currentTime, executionTime;
                task = queue.getMin();
                synchronized(task.lock) {
                    // 校验task状态
                    if (task.state == TimerTask.CANCELLED) {
                        queue.removeMin();
                        continue;
                    }
                    currentTime = System.currentTimeMillis();
                    executionTime = task.nextExecutionTime;
                    
                    // 当前时间 >= 目标执行时间,说明任务可执行,设置taskFired = true
                    if (taskFired = (executionTime<=currentTime)) {
                        if (task.period == 0) { // period == 0 说明是非周期任务,先从队列移除
                            queue.removeMin();
                            task.state = TimerTask.EXECUTED;
                        } else { // 周期任务,会根据period重设执行时间,再加入到队列中
                            queue.rescheduleMin(
                              task.period<0 ? currentTime   - task.period
                                            : executionTime + task.period);
                        }
                    }
                }
                if (!taskFired) // 任务为不需执行状态,则等待
                    queue.wait(executionTime - currentTime);
            }
            if (taskFired)  // 任务需要执行,则调用task的run方法执行,这里执行的其实就是调用方创建task时候run方法的逻辑
                task.run();
        } catch(InterruptedException e) {
        }
    }
}
复制代码

从 mainLoop的源码中可以看出,基本的流程如下所示

队列为空

false

true

队列不为空

CANCELLED

其他

false

true

false

true

检查队列状态

校验newTasksMayBeScheduled

线程退出

wait

校验队首task状态

移除task,下一轮循环

校验任务时间是否可执行

线程wait到待执行时间

是否周期任务

从队列中移除任务

重置下一次任务时间

执行当前任务

当发现是周期任务时,会计算下一次任务执行的时间,这个时候有两种计算方式,即前面API中的

  • schedule:period为负值,下次执行时间
  • scheduleAtFixedRate:period为正值
queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period);
复制代码

优先队列

当从队列中移除任务,或者是修改任务执行时间之后,队列会自动排序。始终保持执行时间最早的任务在队首。 那么这是如何实现的呢?

看一下TaskQueue的源码就清楚了

class TaskQueue {

    private TimerTask[] queue = new TimerTask[128];

    private int size = 0;

    int size() {
        return size;
    }
    
    void add(TimerTask task) {
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);

        queue[++size] = task;
        fixUp(size);
    }

    TimerTask getMin() {
        return queue[1];
    }

    TimerTask get(int i) {
        return queue[i];
    }

    void removeMin() {
        queue[1] = queue[size];
        queue[size--] = null;  // Drop extra reference to prevent memory leak
        fixDown(1);
    }

    void quickRemove(int i) {
        assert i <= size;

        queue[i] = queue[size];
        queue[size--] = null;  // Drop extra ref to prevent memory leak
    }

    void rescheduleMin(long newTime) {
        queue[1].nextExecutionTime = newTime;
        fixDown(1);
    }

    boolean isEmpty() {
        return size==0;
    }

    void clear() {
        for (int i=1; i<=size; i++)
            queue[i] = null;

        size = 0;
    }

    private void fixUp(int k) {
        while (k > 1) {
            int j = k >> 1;
            if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

    private void fixDown(int k) {
        int j;
        while ((j = k << 1) <= size && j > 0) {
            if (j < size &&
                queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
                j++; // j indexes smallest kid
            if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
                break;
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

    void heapify() {
        for (int i = size/2; i >= 1; i--)
            fixDown(i);
    }
}

复制代码

可以看到其实TaskQueue内部就是基于数组实现了一个最小堆 (balanced binary heap), 堆中元素根据 执行时间nextExecutionTime排序,执行时间最早的任务始终会排在堆顶。这样工作线程每次检查的任务就是当前最早需要执行的任务。堆的初始大小为128,有简单的倍增扩容机制。

其他方法

TimerTask 任务有四种状态:

  • VIRGIN: 任务刚刚创建,还没有schedule
  • SCHEDULED:任务已经schedule,进入到队列中
  • EXECUTED: 任务已执行/执行中
  • CANCELLED:任务已取消

Timer 还提供了cancel和purge方法

  • cancel,清除队列中所有任务,工作线程退出。
  • purge,清除队列中所有状态置为取消的任务。

常见应用

Java的Timer广泛被用于实现异步任务系统,在一些开源项目中也很常见, 例如消息队列RocketMQ的 延时消息/消费重试 中的异步逻辑。

public void start() {
    if (started.compareAndSet(false, true)) {
        super.load();
        // 新建了一个timer
        this.timer = new Timer("ScheduleMessageTimerThread", true);
        for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
            // ...
        }
        
        // 调用了Timer的scheduleAtFixedRate方法
        this.timer.scheduleAtFixedRate(new TimerTask() {

            @Override
            public void run() {
                try {
                    if (started.get()) {
                        ScheduleMessageService.this.persist();
                    }
                } catch (Throwable e) {
                    log.error("scheduleAtFixedRate flush exception", e);
                }
            }
        }, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
    }
}

复制代码

上面这段代码是RocketMQ的延时消息投递任务 ScheduleMessageService 的核心逻辑,就是使用了Timer实现的异步定时任务。

三、总结

不管是实现简单的异步逻辑,还是构建复杂的任务系统,Java的Timer确实是一个方便实用,而且又稳定的工具类。从Timer的实现原理,我们也可以窥见定时系统的一个基础实现:线程循环 + 优先队列。这对于我们自己去设计相关的系统,也会有一定的启发。

最后,欢迎大家评论,任何形式的建议和纠错都很欢迎 👏


原文链接:https://juejin.cn/post/7044490907726544933
 

 

Java定时任务(一) Timer及TimerTask的案例解析及源码分析 Java定时任务(一) Timer及TimerTask的案例解析及源码分析 一、概述: 定时任务这个概念在Java的学习以及项目的开发中并不陌生,应用场景也是多种多样。比如我们会注意到12306网站在锁定车票的30分钟以内要进行车票费用的支付,否则订单会被取消;再比如我... 阅读详情

相关推荐

Replay音频转换时samplerate缺失的解决方案指南

Replay音频转换时提示"No module named 'samplerate' This error is lazily reported, having originally occured in File"的解决方案

weixin_47445543的博客 2127

如何实现定时任务- Java Timer/TimerTask 源码原理解析

一、背景和使用     日常实现各种服务端系统时,我们一定会有一些定时任务的需求。比如会议提前半小时自动提醒,异步任务定时/周期执行等。那么如何去实现这样的一个定时任务系统呢? Java JDK提供的Timer类就是一个很好的工具,通过简单的API调用,我们就可以实现定时任务。     现在就来看一下java.util.Timer是如何实现这样的定时功能的。     首先,我们来看一下一个使用demo Timer timer = new Timer(); TimerTask task...

欢迎来到我的博客 1017

Windows 11最新WDK安装避坑指南:从Visual Studio配置到KMDF驱动开发

本文提供了在Windows 11系统上进行KMDF驱动开发的完整环境搭建与实战指南。针对最新WDK与Visual Studio的配置变化,详细解析了从独立安装WDK/SDK、创建首个KMDF驱动项目,到编译排错、部署调试的全流程,帮助开发者高效避坑,顺利开启Windows内核开发。

weixin_29302047的博客 525

(一)定时任务类Timer中schedule的new TimerTask()方法的使用示例(简单的)

项目开发中经常用到定时任务,本篇对定时任务类Timer中schedule的new TimerTask()方法的使用进行说明,以下代码示例内容: 示例一: 需求:10秒之后爆炸一次(用炸弹为例) package com.test.timer; import java.util.Date; import java.util.Timer; import java.util.TimerTask; /*...

u013131716的博客 7042

LabVIew串口收发的一些问题

LabView串口收发实验总结

weixin_45508826的博客 2065

set_data_check专题[使用,report_timing,setup/hold检查]

set_data_check用于约束『数据-对-数据』的『建立保持』时间检查,通常被称为非时序约束 ( non-sequential constraints ), 理论上set_data_check 可以设在任意的两个data pin 上,其中一个pin 称为 constrained pin 类似于寄存器的data pin, 另一个pin 称为related pin 类似于寄存器的clock pin.

cy413026的博客 1万+

java.lang.IllegalArgumentException: Non-positive period.

在项目中用到了TimerTask时间调度器,如果调度间隔时间min为0的时候,则会发生错误,必须要设置大于0 的数。 timer.schedule(timerTask, 25000, 1) 即第三个参数必须大于0

wwslina的博客 469

【雷达成像实战】RD、CS、RMA算法对比与Matlab实现:从信号处理到高清成像

本文深入对比了雷达成像中三种核心算法:经典的距离多普勒算法(RD)、基于稀疏重建的压缩感知算法(CS)以及高精度的快速多通道获取算法(RMA)。通过详细的Matlab实战代码,从原理拆解到实现步骤,分析了各自在计算效率、成像精度、数据需求和适用场景上的优劣,为工程选型与算法实践提供了清晰指南。

weixin_29095587的博客 446

Timer和TimerTask定时器使用

Timer是一种定时器工具,用来在一个后台线程计划执行指定任务。它可以计划执行一个任务一次或反复多次。  TimerTask一个抽象类,它的子类代表一个可以被Timer计划的任务。具体的任务在TimerTask中run接口中实现。  通过Timer中的schedule方法启动定时任务。 一、运行定时器 启动一个定时器实质是启动一个线程  1、在指定日期运行定时器任务,只运行一次    pub...

w592376568的博客 863

71、机器人与机器人控制器技术解析

本文深入解析了机器人控制器的通信接口、传感器数据采集与处理以及抓手技术三大核心技术。详细介绍了数字I/O、串行接口、总线系统和以太网等通信方式,探讨了各类传感器在焊接、装配、质量检测等场景中的应用,并阐述了手眼校准与路径校正的关键方法。同时,文章还分析了不同类型抓手的特点及选型设计原则,展示了机器人在现代工业自动化中的高效性与灵活性。通过实际编程示例与流程图,全面呈现了机器人系统的集成与控制逻辑。

gin88的博客 275

深度拆解 ‘High-Frequency Trading’ (HFT) 系统:解析如何在 100 纳秒内完成从网卡到 C++ 策略的响应

在100纳秒内完成从网卡到C++策略的响应,这是一项对技术栈每一个层面都进行极限优化的系统工程。它要求我们深入理解计算机体系结构、操作系统、网络协议和C++语言的底层机制。从物理距离的极致缩短,到高性能网卡和内核旁路技术,再到C++代码中每一个字节、每一个CPU周期的精打细算,HFT系统展现了人类对计算速度的永恒追求。这项技术不仅仅应用于金融交易,其背后的低延迟理念和实现方法,如DPDK、无锁编程、硬件加速等,正在逐渐渗透到其他对延迟敏感的领域,例如电信网络、边缘计算和工业自动化。

weixin_41455464的博客 1039

Java的定时器Timer和定时任务TimerTask应用以及原理简析

记录:272 场景:Java JDK自带的定时器Timer和定时任务TimerTask应用以及原理简析。在JDK工具包:java.util中可以找到源码,即java.util.Timer和java.util.TimerTask。TimerTask实现Runnable接口的run方法。Timer的属性TimerThread thread继承Thread。因此,Timer天生就具备多线程属性。这个轻量级的定时器和定时任务,在多线程的场景中使用极其便利和灵活。...

zhangbeizhen18的博客 6316

DA14531之搭建开发环境

DA14531之搭建开发环境

hejinwu1314的博客 1776

【Java】定时任务 - Timer/TimerTask 源码原理解析

【Java】定时任务 - Timer/TimerTask 源码原理解析

blblccc 1254

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Java Timer/TimerTask 源码解析

。

博学笃志-格物明德 782

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

Java的Timer定时任务解析

Java的定时任务Timer类解析

Joker_Lii的博客 1873

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

JAVA定时任务 Timer TimerTask 笔记

JAVA定时任务 Timer TimerTask 笔记前言Timer + TimerTask 简单用法说明例子:延时三秒后执行一次:延时三秒后, 每秒执行一次, 用scheduleAtFixedRate方法:延时三秒后, 每秒执行一次, 用schedule方法:java.util.Timer基本原理主要方法构造方法Timer()Timer(String name)Timer(boolean isDaemon)Timer(String name, boolean isDaemon)构造方法的 isDaemon

kfepiza的博客 4960

java timertask 定时_Java定时任务--Timer和TimerTask

1.程序和编程我们知道,计算机程序是一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具。简单来说程序在本质上是指令的集合,而编程从字面上来讲就是编写这些指令集合(程序),通过程序去完成某种行为。在日常编程中,我们难免会遇到这种情况,如何在指定的时间去做某一件事情(比如闹钟响铃)?在Java中通常使用定时任务去实现,接下来会结合源码分析Java中的定时任务。2.从段子开始...

weixin_35524300的博客 1341

Timer与TimerTask的基本使用和源码解析

前言 Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。 ...

weixin_43947801的博客 811

Java中Timer和TimerTask使用

Java提供了轻量级的定时任务实现Timer,通过Timer我们可以实现定时执行任务,定时循环执行任务的功能,我们先看JavaDoc中关于Timer的说明 一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。   与每个 Timer 对象相对应的是单个后台线程,用于顺序地执行所有计时器任务。计时器任务应该迅速完成。如果完成某个计时器任务的时间太长...

换了马甲的小强的博客 4441

Java定时任务1_定时任务实现方式以及原理

背景背景在 Java 应用开发中,定时任务是一种常见的需求,它可以帮助我们自动化执行周期性的工作,如试卷拉取、检测超时任务退回、数据备份、定时通知等。Java 提供了多种方式来实现定时任务,如使用 Timer、ScheduledExecutorService 以及 Spring 框架中的 @Scheduled 注解等,或者更重一些的xxl-job等。

xiaosi的博客 1055

Java定时任务 - Timer 原理

Java定时任务 - Timer 原理 概要 Jdk库自带有两种实现定时任务的技术。一种是通过Timer,另外一个是通过ScheduledThreadPoolExecutor。下面为大家分析Timer实现的原理。 一、Timer 1、Timer使用 public class TimerTest extends TimerTask { @Override public void run() { System.out.println("test1 --------- " + T

weixin_44050303的博客 989

Timer与TimerTask的真正原理&使用介绍

其实就Timer来讲就是一个调度器,而TimerTask呢只是一个实现了run方法的一个类,而具体的TimerTask需要由你自己来实现,例如这样: Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { System.out.println("abc"); } }, 200

xieyuooo的专栏 6万+

java Timer(定时调用、实现固定时间执行)

最近需要用到定时调用的功能。可以通过java的Timer类来进行定时调用,下面是有关Timer的一些相关知识。 其实就Timer来讲就是一个调度器,而TimerTask呢只是一个实现了run方法的一个类,而具体的TimerTask需要由你自己来实现,例如这样: Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { System.out.println("11232

wang704987562的博客 3550

Java并发编程:Timer和TimerTask

下面内容转载自:   http://blog.youkuaiyun.com/xieyuooo/article/details/8607220   其实就Timer来讲就是一个调度器,而TimerTask呢只是一个实现了run方法的一个类,而具体的TimerTask需要由你自己来实现,例如这样: 1 2 3 4 5 6

samjustin的博客 961
上一篇: 图解四种 IO 模型 轻松区分BIO、NIO 和 AIO 、SIGIO
下一篇: 和阿里面试官谈JVM调优,他居然把我的电话挂了
Java架构师小森
博客等级 码龄5年 23粉丝 32原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值