三、线程安全

  • 局部变量是线程安全的。

  • 但是局部变量引用的对象则不一定

    • 如果对象没有逃离方法的作用范围,那么是线程安全的。
      • 如果对象逃离了方法的作用范围,需要考虑是不是线程安全。 例如:引用了堆中的对象,会被共享。

3)局部变量线程安全分析?

1. 普通局部变量
public static void test1() {
	int i = 10;
    i ++;
}

这个代码不会出现线程安全问题

每一个线程会有自己对应的栈空间,每个线程调用test1()方案时局部变量i,会在每个线程自己的栈空间调用,进行压栈,所以各自调用自己的,互不干扰,不存在共享。

2. 局部变量引用
public static void main(String[] args) {
    ThreadUnsafe tes = new ThreadUnsafe();
    for (int i = 0; i < 2; i ++ ) {
        new Thread(() -> {
            test.method1(200);
        }, "Thread" + (i + 1)).start();
}

class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++ ) {
            method2();
            method3();
        }
    }

    private void methdo2() {
        list.add("1");
    }
    
    private void methdo3() {
        list.remove(0);
    }
}

上面的代码中,list是成员变量,所以是共享资源,也就是临界区,临界区中的代码如果不加以限制,多线程情况下,会造成执行顺序不可预测,发生竞态条件

所以上面的代码会有线程安全的问题,会导致发生数组下表越界异常(同时执行remove操作,这时候就会发生错误)

解决方法

需要确保只能有一个线程能够执行,或者将成员变量 变成 局部变量。

3. 暴露引用对象

如果创建一个子类 继承 ThreadUnsafe类,然后子类对 method2 或者 method3 进行重写,创建一个新的线程执行

这时候list这个局部变量就暴露了, 也就是在子类中的一个新的线程中被引用到了,这时候list就是一个共享资源,也就是临界区,那么就会发生线程安全问题

public static void main(String[] args) {
    ThreadUnsafe tes = new ThreadUnsafe();
    for (int i = 0; i < 2; i ++ ) {
        new Thread(() -> {
            test.method1(200);
        }, "Thread" + (i + 1)).start();
}

class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++ ) {
            method2();
            method3();
        }
    }
	//private
    public void methdo2() {
        list.add("1");
    }
    //private
    public void methdo3() {
        list.remove(0);
    }
}
    
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

解决方案:

可以通过将父类中的方法权限修饰符进行修改,变成private或者final等,子类就不能够进行重写,这样就不会导致线程安全问题。

4. 常见线程安全类
  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的

Hashtable table = new Hashtable();

new Thread(() -> {
    table.put("key", "value1");
}).start();

new Thread(() -> {
    table.put("key", "value2");
}).start();

可以看源码,是添加了synchronized锁,保证了原子性

但是线程安全是调用单一方法

如果多个方法组合调用 ,那么将就不是线程安全的了

4.1. 线程安全类方法的组合
Hashtable table = newHashtable();

if (table.get("key") == null) {
    table.put("key", value);
}

线程1跟线程2同时访问上面的代码

单独访问put跟get方法是有原子性的,但是两个组合起来就不是了

4.2. 不可变类线程安全性

String、Integer是不可变类,所以其内部状态是不可修改的,因此他们的方法都是线程安全的

疑问:String 中有 substring等方法不是可以修改他的值吗

substring是创建一个新的值,所以不会对原本字符串进行修改。

4.3. 案例分析

继承了HttpServlet,Servlet是Tomcat中的,只能有一个实例,所以omcat中的多线程调用的时候共享使用,就会发生线程安全得问题

例1:

例2:

最好对count有一些保护,防止称为临界区。

例3:

例4:

例5:如果将例4中的Connection 写成成员变量,不是局部变量,那么就会有线程安全问题

因为 servlet 只有一份,导致 userservice只有一份,所以UserDao也只有一份,所以多线程访问的时候,就会导致可能第二个线程close链接,第一个线程就拿不到了。

例6:

所以平时书写的时候,不想往外暴露的就写成final,或者private私有的,可以增强安全性。

String 类是不可变的,但是他也是写成final,防止发生继承之后覆盖行为,修改了。这也是很经典的 闭合原则

五、Monitor 概念

1)Java 对象头

例子:

int 占用 4个字节

Integer 占用 8个对象头 + 4 个int值字节 12字节

2)Monitor (锁)

Monitor 被翻译成 监视器 或 管程(操作系统层面)

  • 刚开始的时候Monitor 为null
  • 当 第一个线程 执行到 synchronized(obj)的时候,因为是第一个,所以obj对象的 对象头中的 MarkWord就会通过 指针的形式关联一个 Monitor ,然后将当前线程 设置成 Monitor中的Owner,表示现在的所有者,只能有一个Owner
  • 然后第二个线程来执行的时候,执行到 synchronized(obj)的时候,发现obj 关联的 Monitor 已经有 Owner 了,这时候就会将第二个线程放到 EntryList 中阻塞等待,相当于放到一个阻塞队列中,进入BLOCKED状态
  • 然后第三个线程来执行也是相同,进入EntryList 中阻塞等待,以此类推。。。
  • 然后等第一个线程的同步代码执行完毕之后,就会唤醒 EntryList中等待的线程来竞争锁,这是一种不公平的挑选,不是先来先出的。

注意:

  • synchronized 必须是进入同一个对象的monitor才能有上述的效果
  • 不加synchronized 的对象不会关联监视器,不遵从上述规则

3)synchronized 优化原理

1. 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁 使用者是没有感知的,语法仍然是 synchronized

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
        //同步块 A
        method2();
    }
}
public static void method2() {
	synchronized( obj ) {
        //同步块B
    }
}

原理:

2. 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变成重量级锁

static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
        //同步块
    }
}

所谓的锁膨胀,也就是在将来的解锁操作 进入一个重量级锁的解锁操作

根据上图进行解释分析:

  • 当线程1 加轻量级锁失败,进入了锁膨胀流程
  • 这时候Object 对象就会申请一个Monitor锁,并且让Object 的对象头修改,指向重量级锁的地址
  • 然后线程1 就进入Monitor的 EntryList 阻塞队列 BLOCKED,这样就不会让线程1干耗着

  • 当线程0 退出同步代码块进行解锁时,使用CAS将Mark Word 的值恢复给对象头,这时候就恢复失败了,因为Monitor的地址已经不是所记录的地址了
  • 这时候经过了锁膨胀,已经是重量级锁的地址了,所以需要进行一个重量级锁的解锁操作,通过对象头地址找到Monitor, 将Monitor的Owner置为 空, 然后唤醒 EntryList 中的线程
3. 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,也就是会进行几次循环重试。

如果当前线程自旋成功(即这时候持锁线程已经推出了同步块,释放了锁),这时候当前线程就可以避免阻塞了。

因为阻塞会导致上下文切换,性能影响比较大。

自旋只有在多核cpu的情况下才有用,如果单核就没有意义。一个cpu执行同步代码块,另一个线程都没有cpu执行循环,所以没有意义

4. 偏向锁

轻量级锁在没有竞争时(就自己当前线程在运行),每次重入仍然需要执行CAS操作,CAS肯定是执行失败,但是知道是自己线程,所以会保留下来,有性能损耗

Java 6 中引入了偏向锁来做进一步优化: 只有第一次使用CAS 将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。

以后只要不发生竞争,这个对象就归该线程所有。

4.1. 偏向状态

貌似对象的hashcode是懒生成的,当调用hashcode()方法获取 hashcode 值的时候才对对象头里面写入hashcode值,一旦hashcode已经写入,无法使用偏向锁进而使用轻量级锁等

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建之后,markword的值为 0x05 也就是 最后 3 位 101,这时它的thread、 epoch、age 都为0
  • 偏向锁默认是延迟开启的,不会再程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -xx:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果对象在一开始创建的,那么就是没有开启偏向锁,markword最后3位为001,这时候在延迟之后输出他的markword也还是之前的001,需要重新创建的才有效
  • 如果没有开启偏向锁,那么对象创建之后,markword值为0x01 也就是001,这时它的 hashcode、age都为0,第一次用到hashcode才会赋值。
  • 如果一个对象调用开启了偏向锁,调用synchronized之后使用的是偏向锁,那么前面54位地址操作系统会默认分配一个线程id,表示这个对象就给当前线程用了,锁代码同步块执行完毕之后,那54位地址还是执行当前线程,除非有其他线程来竞争,不然一直表示给当前线程使用。但是轻量级锁释放之后就会恢复。
  • 如果禁用偏向锁 ,添加 VM 参数 -xx:-UseBiasedLocking(禁用)/ +UseBiasedLocking(启用)

锁使用优先级:

偏向锁 > 轻量级锁 > 重量级锁

4.2. 撤销 - 调用对象 hashCode

线程调用hashcode() 方法之后,根据对象头格式,偏向锁有54位存储线程id,没有多余的地方存储31位的hashcode码,所以会将thread、epoch清空,转成正常Normal对象。

敲黑板:偏向锁和hashcode是互斥存在的;

  • 轻量级锁的hashcode存储在线程栈帧的锁记录中;
  • 重量级锁的hashcode存储在Monitor对象中!
4.3. 撤销 - 其他线程使用对象

当一个线程使用了当前对象,如果使用的是偏向锁,那么会在thread中记录当前线程的id,表示这个对象给当前线程用

这时候如果有另一个线程来访问这个对象,这时候发现上面有偏向锁,偏向了某一个线程,这时候会撤销偏向锁,改成轻量级锁,然后记录锁地址。

最后解锁之后,无锁状态。

4.4. 撤销 - 调用 wait / notify
  • 想要使用wait / notify 这种机制, 只有重量级锁才有
  • 所以偏向锁,轻量级锁都会升级成重量级锁。
4.5. 批量重偏向
  • 如果对象被多线程访问,但是没有竞争,这时偏向了线程 T1 的对象仍然有机会重新偏向 T2,重偏向会重置对象的 Thread ID,这是一个批量重偏向优化。
  • 比如说对象已经偏向了 线程 T1 ,记录了 T1的Thread ID,然后等执行结束之后,线程 T2 来执行,会发现已经被T1用了偏向锁,这时候JVM会撤销偏向锁,改成轻量级锁
  • 但是当撤销阈值达到超过20次之后,JVM觉得是不是偏向错了,于是会给这些对象加锁时给予重新偏向至加锁线程的能力。
4.6. 批量撤销
  • 当撤销偏向锁操作达到阈值40次之后,JVM会觉得是真的偏向错了,这时候会将整个类的所有对象都设置成不可偏向,新创建的对象也是。
4.7. 锁消除

我们知道加锁,不管了怎么优化,偏向锁,轻量级锁,都会对性能有锁损耗,但是为什么执行代码耗时一样。

这时候就涉及到了JVM了对象逃逸,

我们Java程序是对字节码通过解释 + 编译的方式来执行的,但是对于其中的一些热点代码,会进行一个优化

这时候就涉及 JIT即时编译器 ,会将热点代码进一步翻译成机器码,缓存起来,以后执行就不用 通过编译了

另外他的一个优化手段就是去分析这个局部变量是不是可以优化,发现根本不会逃离方法作用范围,那就不会共享,那么加锁就没有意义,所以Jit 即时编译器会直接将synchronized优化去掉,只是执行了锁中的代码块的代码。

锁消除参数默认开启,如果需要关闭可以使用功能下面 VM 参数

六、wait notify

1) 原理

2)API介绍

wait()、notify()、 notifyAll() 方法都是属于Object 对象的方法,需要获取此对象的锁 之后才能够使用

wait() 对象调用wait() 方法之后,线程会 Owner中释放锁,然后进入WaitSet中等待唤醒

wait( time ) 对象调用带参数的wait()方法之后,线程会 Owner中释放锁,然后进入WaitSet中等待 指定时间,然后如果期间没有被唤醒,指定之间之后就会自动唤醒,然后进入EntryList再次尝试获取锁,竞争锁

notify () 对象调用notify()方法之后,会随机挑选一个 WaitSet 中的一个线程唤醒,然后进入EntryList 竞争锁

notifyAll() 对象调用notifyAll()方法之后,会 唤醒 WaitSet 中所有线程,然后进入EntryList中竞争锁。

wait(long n) 进入 TIMED_WAITING 状态

wait( ) 进入 WAITING状态

3)wait notify 的正确使用姿势

1. sleep vs wait
  • sleep是Thread的静态方法, wait是 Object 的方法
  • sleep 可以随意使用不需要配合synchronized使用, wait需要配合synchronized使用
  • sleep 如果是在synchronized中使用,也是不会释放锁的,wait会释放锁
  • 共同点: 都是进入TIMED_WAITING 状态, 有时间的等待
2. 正确使用姿势
synchronized(lock) {
    while(条件不成立) {
        lock.wait();
    }
    //干活
}
//另一个线程
synchronized(lock) {
    lock.notifyAll();
}

3. 设计模式 之 同步模式保护性暂停
3.1. 定义

3.2. 实现
class GuardedObject {
    //结果
    private Object response;
	
    //获取结果
    public Object get() {
        synchronized (this) {
            //没有结果
            while(response == null) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    //产生结果
    public void generation(Object response) {
        synchronized (this) {
            this.response = response;
            this.notifyAll();
        }
    }
}
public static void main(String[] args) {
    GuardedObject guardedObject = new GuardedObject();
    new Thread(() -> {
        System.out.println(guardedObject.get());
    }).start();

    new Thread(() -> {
        int x = 111;
        guardedObject.generation(x);
    }).start();
}

相对于join的好处

join 需要等待线程执行的结束之后,才能唤醒自己的线程,需要是全局变量 等待另一个线程的结束

保护性暂停等待 设计模式不需要完全等待线程执行结束,可以线程执行到一半的时候就响应线程,从而唤醒自己线程,继续执行,可以是局部变量 等待另一个线程的结果

3.3. 功能增强(超时)
class GuardedObject {
    //结果
    private Object response;
	
    //获取结果
    public Object get(long timeout) {
        synchronized (this) {
            //记录一个初始时间
            long begin = System.currentTimeMillis();
            //经过时间
            long access = 0;
            //没有结果
            while(response == null) {
                long waitout = timeout - access;
                if (waitout <= 0) {
                    break;
                }
                try {
                    this.wait(waitout);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                access = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    //产生结果
    public void generation(Object response) {
        synchronized (this) {
            this.response = response;
            this.notifyAll();
        }
    }
}
3.4. join 原理 源码

join 实现源码跟 超时增强是一模一样的

在java中,Thread类线程执行完run()方法后,一定会自动执行notifyAll()方法。因为线程在die的时候会释放持用的资源和锁,自动调用自身的notifyAll方法。

3.5. 扩展2

刚才思路的问题:

一个线程通过一个 GuardedObject 对象来进行通信, 通过参数的形式来传递,多个线程之间传递来传递去,非常不方便

实现解耦:

通过设计一个集合来管理多个 GuardedObject ,每个给予一个id,用于区分,然后 生产供给, 获取所需。

class Mailboxes{
    //集合
    private static Map<Integer, GuardedObject> map = new Hashtable<>();
    //id
    private static int id = 1;

    private static synchronized int geterateId() {
        return id ++;
    }

    public static GuardedObject createGuardedObject() {
        GuardedObject guardedObject = new GuardedObject(geterateId());
        map.put(guardedObject.getId(), guardedObject);
        return guardedObject;
    }

    public static GuardedObject getGuardedObject(int id) {
        return map.remove(id);
    }

    //获取所有GuardedObject
    public static Set<Integer> getIds() {
//        System.out.println(map.keySet());
        return map.keySet();
    }
}
class GuardedObject {
    //id
    private int id;
    //结果
    private Object response;

    public GuardedObject(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    //获取结果
    public Object get(long timeout) {
        synchronized (this) {
            //记录一个初始时间
            long begin = System.currentTimeMillis();
            //经过时间
            long access = 0;
            //没有结果
            while(response == null) {
                long waitout = timeout - access;
                if (waitout <= 0) {
                    break;
                }
                try {
                    this.wait(waitout);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                access = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    //产生结果
    public void generation(Object response) {
        synchronized (this) {
            this.response = response;
            this.notifyAll();
        }
    }
}
class People extends Thread {
    @Override
    public void run() {
        //准备收信
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
       System.out.println("准备收信" + guardedObject.getId());
        Object res = guardedObject.get(5000);
        //收到信
        System.out.println("收到信" + res);
    }
}


class Postman extends Thread {

    private int id;

    private String mail;

    public Postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        //开始送信
//        System.out.println("送信" + id + "内容" + mail);
        GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
        System.out.println("送信" + id + "内容" + mail);
        guardedObject.generation(mail);
    }
}
public class Main{
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i ++ ) {
            new People().start();
        }
        Thread.sleep(1);
//        System.out.println(Mailboxes.getIds());
        for (Integer id : Mailboxes.getIds()) {
//            System.out.println(id + "内容");
            new Postman(id, id + "内容").start();
        }
    }
}

特点: 产生结果线程 和 使用结果线程是一一对应的。

4. 设计模式 之 异步模式 生产者 / 消费者
4.1. 定义

为什么这里是异步,保护性暂停模式却是同步?

  • 因为保护性暂停是一一对应的,只要产生了结果,我就就能立刻拿到进行处理,所以是同步的
  • 但是生产者/消费者 产生了结果之后放入消息队列,如果前面有结果未处理,需要等待,不能立刻执行,所以称为异步
4.2. 实现

final class MessageDeque {

    private static LinkedList<Message> list = new LinkedList<>();

    private static int capacity;

    public MessageDeque(int capacity) {
        this.capacity = capacity;
    }

    //存放消息
    public void put(Message message) {
        synchronized (list) {
            //如果没有满
            while (list.size() == capacity) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("生产者线程等待结束, 没有满, 放入");
            list.addLast(message);
            list.notifyAll();
            System.out.println("放入结束");
        }
    }
    //取出消息
    public Message take() {
        synchronized (list) {
            //如果没有消息
            while(list.isEmpty()) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            Message message = list.removeFirst();
            System.out.println("消费者等待结束, 拿出消息" + message.getId());
            list.notifyAll();
            return message;
        }
    }
}


final class Message {
    private int id;
    private Object mail;

    public Message(int id, Object mail) {
        this.id = id;
        this.mail = mail;
    }

    public int getId() {
        return id;
    }

    public Object getMail() {
        return mail;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", mail=" + mail +
                '}';
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {



还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!


王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。


对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!


【完整版领取方式在文末!!】


***93道网络安全面试题***


![](https://img-blog.csdnimg.cn/img_convert/6679c89ccd849f9504c48bb02882ef8d.png)








![](https://img-blog.csdnimg.cn/img_convert/07ce1a919614bde78921fb2f8ddf0c2f.png)





![](https://img-blog.csdnimg.cn/img_convert/44238619c3ba2d672b5b8dc4a529b01d.png)





内容实在太多,不一一截图了


### 黑客学习资源推荐


最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!


对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。


😝朋友们如果有需要的话,可以联系领取~

#### 1️⃣零基础入门


##### ① 学习路线


对于从来没有接触过网络安全的同学,我们帮你准备了详细的**学习成长路线图**。可以说是**最科学最系统的学习路线**,大家跟着这个大的方向学习准没问题。


![image](https://img-blog.csdnimg.cn/img_convert/acb3c4714e29498573a58a3c79c775da.gif#pic_center)


##### ② 路线对应学习视频


同时每个成长路线对应的板块都有配套的视频提供:


![image-20231025112050764](https://img-blog.csdnimg.cn/874ad4fd3dbe4f6bb3bff17885655014.png#pic_center)


#### 2️⃣视频配套工具&国内外网安书籍、文档


##### ① 工具


![](https://img-blog.csdnimg.cn/img_convert/d3f08d9a26927e48b1332a38401b3369.png#pic_center)


##### ② 视频


![image1](https://img-blog.csdnimg.cn/img_convert/f18acc028dc224b7ace77f2e260ba222.png#pic_center)


##### ③ 书籍


![image2](https://img-blog.csdnimg.cn/img_convert/769b7e13b39771b3a6e4397753dab12e.png#pic_center)

资源较为敏感,未展示全面,需要的最下面获取

![在这里插入图片描述](https://img-blog.csdnimg.cn/e4f9ac066e8c485f8407a99619f9c5b5.png#pic_center)![在这里插入图片描述](https://img-blog.csdnimg.cn/111f5462e7df433b981dc2430bb9ad39.png#pic_center)


##### ② 简历模板


![在这里插入图片描述](https://img-blog.csdnimg.cn/504b8be96bfa4dfb8befc2af49aabfa2.png#pic_center)

 **因篇幅有限,资料较为敏感仅展示部分资料,添加上方即可获取👆**




**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以点击这里获取](https://bbs.csdn.net/topics/618540462)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 24
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值