1.创建者模式-单例模式

一、创建者模式

1.1【概念】

创建者模式的主要关注点是 “怎样创建对象” ?为的就是将对象的创建与使用分离

这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

1.2【分类】

创建者模式分为:

  • 单例模式
  • 工厂方法模式
  • 抽象工厂模式
  • 原型模式
  • 建造者模式

1.3 【叠Buff】

  1. 本文会涉及到一些与JVM和多线程相关的知识点,用于加深理解,大家可以根据自己的情况选择阅读。
  2. 本文内容会比较多,篇幅较长,希望大家能够静下心来读完,栓Q!

二、单例模式

2.1 【概念】

单例模式(singleton pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式可以直接访问,不需要实例化该类的对象。

这晦涩难懂的概念,我相信大家第一次读完了的状态就是,这些字都认识,但是连在一起,就不知道他是做什么的了。

无需紧张,我第一次看完也懵圈,我们先把概念介绍完,有了一个大致的印象以后,通过案例来加深理解。

2.2 【单例模式的分类】

  • 饿汉式
    • 类加载就会导致该单实例对象被创建。
  • 懒汉式
    • 类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。

饿汉式有两种实现方式:

<这里插一嘴题外话>

大家在看到饿汉式的概念时有什么联想?即:类加载就会导致该单实例对象被创建。

我相信大家学过jvm的都应该会产生一点点的共鸣。一提到类加载,我们脑子里就会想到类的生命周期,对不对?

<知识点拓展-类的生命周期>

类的生命周期分为三个阶段:加载阶段,连接阶段,初始化阶段

  1. 加载阶段

    • 根据类的全限定名把字节码文件的内容加载并转换成合适的数据放入内存中,存放在方法区和堆上
  2. 连接阶段

    • 验证:魔数、版本号等验证,一般不需要程序员关注
    • 准备:为静态变量分配内存并设置初始值
    • 解析:将常量池中的符号引用(编号)替换为直接引用(内存地址)
  3. 初始化阶段

    • 执行静态代码块和静态变量的赋值

上面只是简单的介绍一下类的生命周期,日后我会继续写关于jvm的相关笔记的。请大家关注下我,谢谢!!!

我们的目光现在聚焦到:初始化阶段–>“执行静态代码块和静态变量的赋值。” 能想到什么,是不是就和 饿汉式的概念 " 类加载就会导致该单实例对象被创建。"

是不是知识点就对应上了?

没错:饿汉式有两种实现

  1. 静态变量的方式
  2. 静态代码块的方式

其实 饿汉式单例模式发生在类的初始化阶段,类的加载阶段其实是没干什么,就是将字节码文件加载到内存中,这期间是不执行代码的。

饿汉式单例模式的定义之所以这么说,其实是从宏观上的一种表达,更容易理解,但是深挖下去发现,饿汉式单例模式发生在初始化阶段。

在很多情况下,如果不是面试的情况,抠细节的情况,初始化阶段也被视为类加载过程的一部分。不需要特别较真。

讲这个只是单纯的告诉大家,知识之间是有关联性的。

回到我们设计模式上面,下面开始写我们的示例代码:

2.3【饿汉式】

2.3.1【使用静态变量的方式来完成饿汉式】
/**
 * 饿汉式-静态成员变量的方式创建单例
 */
public class Singleton {
    /**
     * 1.私有构造方法-->只有私有构造方法后,外界就访问不到这个构造方法,就无法创建对象
     */
    private Singleton() {

    }

    /**
     * 2.在本类中创建一个本类对象。
     */
    private static final Singleton INSTANCE = new Singleton();

    /**
     * 3.提供一个公共的静态方法,获取本类对象。不能创建对象就不可以调用非静态的方法
     */
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

public class Test {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        //获取到的两个Singleton对象是同一个对象
        System.out.println(instance == instance2); //true
    }
}
2.3.2【使用静态代码块的方式来完成饿汉式】
public class Sigleton {
    private static final Sigleton instance;
    private Sigleton() {
    }
    static {
        instance = new Sigleton();
    }
    public static Sigleton getInstance() {
        return instance;
    }
}

public class Test {
    public static void main(String[] args) {
        Sigleton instance = Sigleton.getInstance();
        Sigleton instance2 = Sigleton.getInstance();
        System.out.println(instance == instance2);//true
    }
}

2.3.3 枚举方式

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会加载一次,设计者充分的利用了枚举的这个特性来实现单例模式。枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。(这个后面说)

枚举类实现单例的优点:

  1. 线程安全:
  • 枚举类在加载时就会对枚举值进行初始化,这一过程由JVM保证是线程安全的,因此无需额外的同步措施,避免了多线程环境下的线程安全问题。
  1. 防止反序列化问题:
  • 枚举类在JDK内部实现了序列化机制,并且重写了readObject()和writeObject()方法,或者通过readResolve()方法,确保了反序列化时不会生成新的实例,从而避免了单例模式在序列化过程中被破坏的风险。
  1. 防止反射攻击:
  • 枚举类的构造方法是私有的,并且JVM禁止通过反射机制调用枚举类的私有构造方法,这进一步增强了单例模式的安全性,防止了通过反射攻击产生新的实例。
  1. 实现简单:
  • 枚举类实现单例模式非常简单,通常只需要定义一个枚举类型,并在其中声明一个枚举常量即可。这种方式代码简洁易懂,易于维护。
  1. 性能高效:
  • 枚举类在JDK内部有特殊的优化,使得枚举实例的访问非常高效。同时,由于枚举常量在类加载时就已经初始化,因此也避免了懒汉式或饿汉式单例模式中的延迟初始化开销。

枚举类的好处

  1. 代码可读性高:
    枚举类可以将一组相关的常量集中在一起,并通过有意义的名称来表达其含义,从而提高了代码的可读性。
  2. 类型安全:
    枚举类型是一种强类型,编译器可以对其进行类型检查,避免了使用错误的值,提高了代码的安全性。
  3. 易于维护:
    当需要修改常量集合时,只需在枚举定义中进行修改即可,无需在代码中的多个地方进行修改,从而简化了维护工作。
  4. 可扩展性:
    通过添加新的枚举常量,可以轻松地扩展现有的常量集合,而不会对现有代码产生影响。

代码:

public enum Singleton {
    /**
     * 单例对象
     */
    INSTANCE;
}

public class Test {
    public static void main(String[] args) {
        Singleton instance1 = Singleton.INSTANCE;
        Singleton instance2 = Singleton.INSTANCE;
        System.out.println(instance1 == instance2);
    }
}

枚举类在类加载时就已经完成了所有枚举值的实例化,所以这种方式实现的单例模式是饿汉式的。

在不考虑浪费内存空间的时候,首选枚举方式去实现单例。

2.4 懒汉式

【懒汉式】

<开胃小菜>
这里将展示一段和静态变量有关的懒汉式内容,可以当作娱乐看一下

public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        instance = new Singleton();
        return instance;
    }
    public int getUrl(){
        //获取对象的hash值,对象相关的唯一标识
        return System.identityHashCode(instance);
    }
}

public class Test {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        System.out.println(instance.getUrl()); //666641942
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance2.getUrl()); //960604060
        System.out.println(instance.getUrl()); //960604060
    }
}

这里第一次输出的值是666641942,这是Singleton 类中instance变量的唯一标识,但是当我们在调用一次Singleton.getInstance()方法,就会新创建一个对象,将instance的变量值覆盖(因为instance是静态变量,它是属于类的,不属于对象,不是对象中独有的)。所以你会看到后面两个输出的instance都是一样的。

同时也说明了这样写的懒汉式是不对的,每次调用都新创建一个对象。这不符合单例模式。

2.4.1【懒汉式–方式1】
public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

请大家想一想这样写有什么问题吗?线程安全吗?

答案是不安全:如果大家学过多线程,应该会有所了解,当我们的线程1拿到了cpu的执行权,开始执行代码,就是这么巧,执行完instance == null 这个语句了,是null,进入的if语句块中了,就在这时,cpu的执行权到期了,给到了线程2,线程2获取到了cpu执行权,通过的if判断进来了,他判断的时候肯定也是能通过的,因为线程1还没创建,cpu时间片就没了,所以线程2执行成功创建了一个对象,此时线程1拿到时间片开始干活,他又创建了一次对象。这时候线程1和线程2创建的对象就是不同。

最简单的解决办法–改为同步,加锁

2.4.2【懒汉式-方式2】
public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

我们可以看到懒汉模式中我们绝大部分的操作都是读操作,读操作是线程安全的,所以我们
直接粗暴的将这个getInstance()都加上锁了,显得就不是很合适,因为大部分都是读操作,不需要加锁。性能会降低
在这里插入图片描述

我们不需要让每个线程拿到锁才去调用方法,所以我们需要调整加锁的时机,从而就衍生出来一种新的实现模式:双重检查锁模式(DCL)

2.4.3【懒汉式-方式3】
public class Singleton {
    /**
     * 声明singleton类型的变量
     */
    private static Singleton instance;

    /**
     * 私有构造器
     */
    private Singleton(){}

    /**
     * 对外提供公共的访问模式
     */
    public static  Singleton getInstance(){
        //第一次判断,如果instance的值不为null,不需要抢占锁,直接返回对象
        if (instance == null) {
            synchronized (Singleton.class) {
                //第二次判断
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

从上面的例子可以看出来,我们加锁范围尽量缩小,不仅是这个例子,如果以后遇到了需要加锁的情况也要注意,在最小职责范围内加锁性,对写操作加锁。

下面我要说的东西会有一些难度,请各位看官老爷认真看。

我们说的双重检查模式这个思想是很不错的思想,我们可以灵活运用到其他的代码中去,但是注意了:在懒汉式单例模式中使用双重检查模式,可能会有一个问题—》空指针问题。

空指针问题出现的地方就是下面这块代码

instance = new Singleton();

这块代码并不是原子性操作。

原子性是指:一个操作或多个操作要么全部执行,要么全部不执行,不会被其他线程中断。

他的正确执行顺序分为三步:

  1. 分配内存:为Singleton对象分配内存空间。
  2. 初始化对象:调用Singleton的构造函数,初始化对象内部的成员字段。
  3. 设置对象引用:将分配好的内存空间的地址赋值给instance变量,使得instance变量指向新创建的Singleton对象。

但是jvm具有指令重排的优化机制,为了能够达到优化的效果,可能会将这个顺序打乱,。如果他的顺序变成了这样:

  1. 分配内存:为Singleton对象分配内存空间。
  2. 设置对象引用:将分配好的内存空间的地址赋值给instance变量(此时对象还未初始化)。
  3. 初始化对象:调用Singleton的构造函数,初始化对象内部的成员字段。

如果在线程A执行到设置对象引用之后但在对象初始化完成之前被挂起,而线程B此时检查到instance不为null(因为引用已经设置,但对象还未初始化),就可能会尝试使用还未完全初始化的对象,从而导致空指针异常。

为了解决这个问题,可以在instance变量前加上volatile关键字。

volatile关键字的作用:

  1. 保证变量的可见性:

a. 当多个线程同时访问一个共享变量时,如果没有适当的同步机制,一个线程对变量的修改可能不会被其他线程立即看到,导致数据不一致。使用volatile修饰变量后,任何线程对变量的修改都会立即刷新到主内存中,而其他线程每次使用变量前都会直接从主内存中读取最新的值,从而保证了可见性。

  1. 禁止指令重排序:

b. 编译器和处理器在优化代码时,可能会对指令进行重排序以提高执行效率。但在多线程环境下,这种重排序可能导致线程安全问题。volatile通过插入内存屏障来禁止指令重排,确保程序的执行顺序与源代码中的顺序一致。

最终修改后的代码:

public class Singleton {  
    // volatile关键字确保变量跨线程的可见性  
    private static volatile Singleton instance;  
  
    private Singleton() {  
        // 私有构造函数防止被实例化  
    }  
  
    public static Singleton getInstance() {  
        if (instance == null) { // 第一次检查  
            synchronized (Singleton.class) {  
                if (instance == null) { // 第二次检查  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}
2.4.4【懒汉式-方式4】

使用静态内部类的方式,静态内部类单例模式中实例由内部类创建,由于在加载外部类的过程中,是不会加载静态内部类的,只有静态内部类的属性/方法被调用时才会被加载,并初始化其静态属性。

静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

静态内部类是外部类的一个静态成员,这意味着你可以在不创建外部类实例的情况下访问它。由于静态内部类与外部类的实例无关,因此它通常用于封装那些与外部类密切相关但不依赖于外部类实例的功能或数据。

代码如下:

public class Singleton {  
  
    private Singleton() {  
         // 私有构造函数防止被实例化  
    }  
  
    // 静态内部类,用于持有单例实例  
    private static class SingletonHolder {  
        // 静态属性,用于存储单例实例,在内部类中声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();  
    }  
  
    // 公共静态方法,用于获取单例实例  
    public static Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
}  

public class Main {  
    public static void main(String[] args) {  
        Singleton singleton1 = Singleton.getInstance();  
        Singleton singleton2 = Singleton.getInstance();  
  
        // 输出相同的实例引用,证明是单例  
        System.out.println(singleton1 == singleton2); // true  
    }  
}

当我们多次调用 getInstance() 方法时,具体的执行步骤如下:

  1. 第一次调用 getInstance():
  • 静态内部类 SingletonHolder 被首次访问,因此它会被加载。
  • 在加载过程中,SingletonHolder 的静态属性 INSTANCE 会被初始化,即创建一个新的 Singleton 实例。
  • getInstance() 方法返回这个新创建的 Singleton 实例。
  1. 第二次及后续调用 getInstance():
  • 由于 SingletonHolder 已经是加载状态,并且其静态属性 INSTANCE 也已经被初始化,因此不会再次创建新的 Singleton 实例。
  • getInstance() 方法直接返回已经存在的 INSTANCE 值。

这时候有同学问了:如果是多线程同时调用getInstance() 方法,会不会出现线程安全问题呀?

可以明确的回复大家:不会,因为类的加载是由JVM负责的,并且JVM保证了类加载的线程安全性。所以我们根本不需要管线程是否安全,JVM都帮我们做完了。

即使多个线程同时尝试加载同一个类,也只会有一个线程成功加载该类,而其他线程会等待该类加载完成。

相比之下,双重检查锁(DCL)模式则需要我们自己来保证线程安全。

静态内部类单例模式相对于双重检查锁模式来说,更加简洁且易于实现,同时也避免了由于手动处理线程同步而可能引入的复杂性和风险。

其实:DCL模式是给我们提供一个比较好的思想,告诉我们如何

【小结】

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费,

三、单例模式存在的问题

3.1 破坏单例模式

3.1.1 序列化反序列化方式破坏单例

【序列化和反序列化】

  1. 序列化:
    将对象状态转换为二进制字节流,以便可以持久化到文件、数据库或通过网络传输。序列化的主要目的是实现对象的深拷贝,使得对象可以在不同的物理位置或时间点上重新创建出来,同时保持对象的状态不变
  2. 反序列化
    是序列化的逆过程,即将二进制字节流转换回原来的数据结构或对象状态。通过反序列化,可以从存储介质或网络传输中读取对象状态,并在内存中重新创建出该对象,使其恢复到序列化之前的状态。

反序列化过程本质上是一个对象创建过程,当这个二进制字节流被反序列化时,会创建一个新的对象实例,而不是恢复原有的单例实例

注意:

序列化前的单例对象和反序列化后得到的对象虽然具有相同的状态信息,但它们在内存中的地址是不同的。这意味着反序列化后得到的对象是一个全新的实例,而不是原有的单例实例。

【代码演示】

public class Singleton implements Serializable {
    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

/**
 * 测试序列化反序列化破坏单例
 */
public class Test {
    public static void main(String[] args) throws Exception {
        //writeObject();
        Singleton singleton1 = readObject(); 
        Singleton singleton2 = readObject();
        Singleton singleton3 = Singleton.getInstance();
        System.out.println(singleton1); //com.yskj.demo7.Singleton@4e515669
        System.out.println(singleton2); //com.yskj.demo7.Singleton@17d10166
        System.out.println(singleton3); //com.yskj.demo7.Singleton@1b9e1916
    }
    /**
     * 从文件中读取对象
     */
    public static Singleton readObject() throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\book\\a.txt"));
        Singleton instance = (Singleton) ois.readObject();
        ois.close();
        return instance;
    }
    /**
     * 向文件中写入对象
     */
    public static void writeObject() throws Exception{
        Singleton instance = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\book\\a.txt"));
        oos.writeObject(instance);
        oos.close();
    }
}

我们可以看到最终输出的结果地址是不同的,代表着反序列化的对象是不同的,这就破坏了单例模式。

3.1.2 反射方式破坏单例
public class Singleton {
    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

/**
 * 测试反射破坏单例
 */
public class Test {
    public static void main(String[] args) throws Exception {
        //获取Singleton获取到的其字节码对象
        Class<Singleton> clazz = Singleton.class;
        /*
         * 获取私有的无参构造方法对象
         * 如果是public的,直接用getConstructor()方法即可
         */
        Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
        /*
         * 上面只是获取到了构造方法的对象,并没有调用,需要设置访问权限
         * 取消访问检查
         */
        constructor.setAccessible(true);
        Singleton instance1 = constructor.newInstance();
        Singleton instance2 = constructor.newInstance();
        System.out.println(instance1 == instance2); //false
    }
}

我们看到了最终的返回结果是false,这就说明,反射也是可以破坏单例模式的。那么我们应该如何去解决这些问题。

四、问题的解决

4.1 解决反序列化破坏单例问题

为了解决序列化破坏单例模式的问题,可以在单例类中添加一个readResolve()方法。这个方法会在反序列化过程中被自动调用,从而允许开发者指定反序列化时应该返回的对象实例。如果不定义,则返回新new出来的对象。

【代码】

public class Singleton implements Serializable {
    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    /**
     * 这个返回Singleton对象或者Object对象都可以
     * 进行反序列化的时候,会调用readResolve方法,将该方法的返回值返回。
     */
    public Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

/**
 * 反序列化问题解决
 */
public class Test {
    public static void main(String[] args) throws Exception {
        writeObject();
        Singleton singleton1 = readObject();
        Singleton singleton2 = readObject();
        Singleton singleton3 = Singleton.getInstance();
        System.out.println(singleton1); // com.yskj.demo9.Singleton@5d6f64b1
        System.out.println(singleton2); // com.yskj.demo9.Singleton@5d6f64b1
        System.out.println(singleton3); // com.yskj.demo9.Singleton@5d6f64b1
    }
    /**
     * 从文件中读取对象
     */
    public static Singleton readObject() throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\book\\a.txt"));
        Singleton instance = (Singleton) ois.readObject();
        ois.close();
        return instance;
    }
    /**
     * 向文件中写入对象
     */
    public static void writeObject() throws Exception{
        Singleton instance = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\book\\a.txt"));
        oos.writeObject(instance);
        oos.close();
    }
}

为什么反序列化会自动调用readResolve()方法呢?我们通过阅读源码的方式来看看,简单的预览下,首先读取数据,即反序列化对象的方法就是readObject()
在这里插入图片描述
让我们点进去看看,我们需要关注:readObject0() 这个方法。
在这里插入图片描述
进来以后我们只需要关注readOrdinaryObject()方法
在这里插入图片描述
在这里插入图片描述

4.2 解决反射破坏单例问题

反射破坏单例的主要问题就是在于它可以跳过访问检查,直接访问我们的私有构造器,所以我们应该在私有构造器中做文章。

【代码-双重检查锁模式】

双重检查锁模式我们可以直接拿到instance变量,直接判断他是否为空。

public class Singleton {
    /**
     * 声明singleton类型的变量
     */
    private static volatile Singleton instance;

    /**
     * 私有构造器
     */
    synchronized (Singleton.class) {
            // 防止反射攻击
            if (instance != null) {
                throw new RuntimeException("单例模式不允许多个实例");
            }
    }

    /**
     * 对外提供公共的访问模式
     */
    public static  Singleton getInstance(){
        //第一次判断,如果instance的值不为null,不需要抢占锁,直接返回对象
        if (instance == null) {
            synchronized (Singleton.class) {
                //第二次判断
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

【代码-静态内部类】

public class Singleton {
    /**
     * 用来标记构造器是否是非第一次被调用,默认为false,代表第一次被调用
     */
    private static boolean flag = false;

    private Singleton() {
        //防止多线程同时调用,造成多对象实例化
        synchronized (Singleton.class) {
            // 防止反射攻击
            // 因为通过静态内部类方式完成的懒汉式,没有instance变量
            if (flag) {
                throw new RuntimeException("单例已被侵犯");
            }
            flag = true;
        }
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

五、源码练习

5.1 JDK源码解析-Runtime类

Runtime类就是使用的单例设计模式。
在这里插入图片描述

5.2 简单使用Runtime类

public class RuntimDemo {
    public static void main(String[] args) throws IOException {
        Runtime runtime = Runtime.getRuntime();
        /*
         * exec(String command):参数是:一条命令
         * jdk18以后就已经弃用exec方法了,推荐使用ProcessBuilder
         * ProcessBuilder pb = new ProcessBuilder("ipconfig");
         * 但是本文是为了展示先Runtime的单例模式
         */
        Process process = runtime.exec("ipconfig");
        InputStream inputStream = process.getInputStream();
        byte[] bytes = new byte[1024];
        while (inputStream.read(bytes) != -1) {
            System.out.println(new String(bytes, "GBK"));
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值