04【单例设计模式】

}


* 测试类:



package com.dfbz.demo03_懒汉式_线程安全问题;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) {

    new Thread(){
        @Override
        public void run() {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(Thread.currentThread().getName() + "【" + singleton + "】");
        }
    }.start();

    new Thread(){
        @Override
        public void run() {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(Thread.currentThread().getName() + "【" + singleton + "】");
        }
    }.start();


    new Thread(){
        @Override
        public void run() {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(Thread.currentThread().getName() + "【" + singleton + "】");
        }
    }.start();
}

}


执行结果:


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


##### 2)解决线程安全问题


为了解决线程安全问题,我们可以使用synchronized关键字来实现线程同步;



package com.dfbz.demo04_懒汉式_synchronized;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Singleton {

private static Singleton singleton = null;

public static Singleton getSingleton() {
    synchronized (Singleton.class){         // 关键的代码锁住(保证同一时间只能有一个线程进来判断)
        if (singleton == null) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            singleton = new Singleton();

        }
        return singleton;
    }
}

}


* 测试类(代码不变):



package com.dfbz.demo04_懒汉式_synchronized;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) {

    new Thread(){
        @Override
        public void run() {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(Thread.currentThread().getName() + "【" + singleton + "】");
        }
    }.start();

    new Thread(){
        @Override
        public void run() {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(Thread.currentThread().getName() + "【" + singleton + "】");
        }
    }.start();


    new Thread(){
        @Override
        public void run() {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(Thread.currentThread().getName() + "【" + singleton + "】");
        }
    }.start();

}

}


再次运行测试类:发现多条线程创建的对象仍是同一个


##### 3)双重校验锁方案


我们前面通过synchronized同步代码块可以实现多线程下同步问题;但是如果在线程数量非常大的情况下,使用synchronized加锁,则会导致大量的线程阻塞,程序性能会大幅下降;想要保证线程安全问题,有想要性能上的提升,我们可以使用**双重校验锁**;


* synchronized方案:


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


* 未进行双重校验:


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


* 双重校验:


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


* 双重校验锁判断:



package com.dfbz.demo05_双重校验锁;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Singleton {

private static Singleton singleton = null;

public static Singleton getSingleton() {
    // 检查是否需要阻塞(并不是所有的线程上来就阻塞)
    if (singleton == null) {

        // 获取到锁的线程才进去
        synchronized (Singleton.class){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 重新判断,因为前面有可能多条线程都通过了if判断
            if(singleton==null){
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

}


* 测试类(代码不变):



package com.dfbz.demo05_双重校验锁;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) {

    new Thread(){
        @Override
        public void run() {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(Thread.currentThread().getName() + "【" + singleton + "】");
        }
    }.start();

    new Thread(){
        @Override
        public void run() {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(Thread.currentThread().getName() + "【" + singleton + "】");
        }
    }.start();

    new Thread(){
        @Override
        public void run() {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(Thread.currentThread().getName() + "【" + singleton + "】");
        }
    }.start();

}

}


再次运行测试类:发现多条线程创建的对象仍是同一个


#### 4.1.4 静态内部类与单例设计模式


##### 1)静态内部类方案


双重校验锁单例写法虽然解决了线程安全问题和性能问题,但是只要用到synchronized关键字总是要加锁,对性能还是存在一定的影响;我们可以从类加载的角度初始化,采用静态内部类来实现单例设计模式;


* 小案例:



package com.dfbz.demo06_静态内部类;

/**
* @author lscl
* @version 1.0
* @intro: 测试静态内部类加载顺序
*/
public class TestInnerClass {

public static class StaticInnerTest {
    static {
        System.out.println("静态内部类加载了....");
    }

    static void staticInnerMethod() {
        System.out.println("静态内部类方法调用了....");
    }
}

}


* 测试类:



package com.dfbz.demo06_静态内部类;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01_静态内部类加载顺序 {
public static void main(String[] args) {

    // 创建外部类时,静态内部类不会随着外部类的加载而加载
    TestInnerClass testInnerClass = new TestInnerClass();

    System.out.println("=======================");

    TestInnerClass.StaticInnerTest.staticInnerMethod();
}

}


输出结果:



=======================
静态内部类加载了…
静态内部类方法调用了…


从输出结果可以得出结论:**加载一个类时,其内部类不会被同时加载。**



> 
> 小知识:类何时被加载?
> 
> 
> * 1)实例化对象
> * 2)创建了子类的实例
> * 3)访问静态方法、静态变量等
> * 4)Class.forName();
> * 5)加载了子类(子类调用静态成员、new对象等情况)
> 
> 
> 


知道了静态内部类的加载顺序后,我们可以利用这一特点来实现懒汉式单例设计模式


* 单例类:



package com.dfbz.demo06_静态内部类;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Singleton {

// 私有化成员
private static Singleton singleton = null;

// 私有构造方法
private Singleton() {}

// 提供公共的访问方法
public static Singleton getSingleton(){
    return Holder.INSTANCE;
}

// 使用静态内部类来初始化
private static class Holder {
    private static final Singleton INSTANCE = new Singleton();
}

}


* 测试:



package com.dfbz.demo06_静态内部类;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02_静态内部类实现单例设计模式 {
public static void main(String[] args) {
Singleton s1 = Singleton.getSingleton();
Singleton s2 = Singleton.getSingleton();

    System.out.println(s1 == s2);           // true
}

}



> 
> Tips:静态内部类方案不会存在线程安全问题;静态内部类单例模式是一种优秀的单例模式,是项目中比较常用的一种单例模式(即使存在后续的问题)。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
> 
> 
> 


##### 2)反射破坏单例设计模式


我们之前的设计方案不管是饿汉式还是懒汉式都是将单例类的构造方法私有,让外部无法直接通过new关键字来实例化对象;但是我们之前学习过反射,通过反射我们可以**在外部访问任意修饰符修饰的任意方法**;也就是说,我们之前设计的单例方案不管哪一种都可以使用反射来破解;


* 反射破解静态内部类方案:



package com.dfbz.demo07_反射破坏单例设计;

import com.dfbz.demo06_静态内部类.Singleton;

import java.lang.reflect.Constructor;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) throws Exception{

    // 获取任意修饰符修饰的构造方法
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();

    // 设置强制访问
    constructor.setAccessible(true);

    Singleton s1 = constructor.newInstance();           // 相当于new
    Singleton s2= constructor.newInstance();

    System.out.println(s1 == s2);           // false
}

}


##### 3)防止反射破坏单例


* 单例类:



package com.dfbz.demo07_静态内部类防止反射破坏单例设计模式;

import java.io.Serializable;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Singleton implements Serializable {

// 私有化成员
private static Singleton singleton = null;

// 私有化构造方法
private Singleton() {
    if (Holder.INSTANCE != null) {
        throw new RuntimeException("不允许创建多个实例");            // 如果通过构造方法来创建对象则抛出异常
    }
}

// 共有访问方法
public static Singleton getSingleton() {
    return Holder.INSTANCE;
}

// 静态内部类
private static class Holder {
    private static final Singleton INSTANCE = new Singleton();
}

}


* 测试类1:



package com.dfbz.demo07_静态内部类防止反射破坏单例设计模式;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) {
Singleton s1 = Singleton.getSingleton();
Singleton s2 = Singleton.getSingleton();

    System.out.println(s1 == s2);
}

}


* 测试类2:



package com.dfbz.demo07_静态内部类防止反射破坏单例设计模式;

import java.lang.reflect.Constructor;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02 {
public static void main(String[] args) throws Exception{

    // 获取任意修饰符修饰的构造方法
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();

    // 设置强制访问
    constructor.setAccessible(true);

    Singleton s1 = constructor.newInstance();           // 相当于new
    Singleton s2= constructor.newInstance();

    System.out.println(s1 == s2);           // false
}

}


#### 4.1.5 序列化与单例设计模式


我们单例对象创建好后,有时候需要将对象序列化到磁盘,这样就可以在文件系统中存储它的状态。反序列化的对象会重新分配内存,即被重新创建。这样则违背了单例模式的初衷,相当于破坏了单例设计模式;


##### 1)反序列化破坏单例设计模式


* 静态内部类方式的单例类:



package com.dfbz.demo08_序列化破坏单例设计模式;

import java.io.Serializable;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Singleton implements Serializable { // 实现Serializable接口

// 私有化成员
private static Singleton singleton = null;

// 私有化构造方法
private Singleton() {
    System.out.println("执行了几次构造方法????");
    if (Holder.INSTANCE != null) {
        throw new RuntimeException("不允许创建多个实例");            // 如果通过构造方法来创建对象则抛出异常
    }
}

// 共有访问方法
public static Singleton getSingleton() {
    return Holder.INSTANCE;
}

// 静态内部类
private static class Holder {
    private static final Singleton INSTANCE = new Singleton();
}

}


* 测试类:



package com.dfbz.demo08_序列化破坏单例设计模式;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) {

    try (
            // 对象输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));

            // 对象输入流
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
    ) {
        // 获取单例对象
        Singleton s1 = Singleton.getSingleton();

        // 写出对象
        oos.writeObject(s1);

        // 反序列化底层并不是空参执行构造方法,因此Singleton对象被创建了两次
        Singleton s2 = (Singleton) ois.readObject();

        System.out.println(s1 == s2);               // false,违反单例设计模式
    } catch (Exception e) {
        e.printStackTrace();
    }
}

}


##### 2)解决反序列化破坏单例设计模式


从之前运行的结果可以发现,反序列化后的对象与之前序列化到文件中的对象并不是同一个,Singleton对象被实例化了两次,;我们如何保证在序列化的情况下也能实现单例设计模式呢?我们可以在单例对象中添加一个readResolve()方法,该方法的返回值是对象被反序列化时返回值;


* 修改单例类:



package com.dfbz.demo09_解决反序列化破坏单例;

import java.io.Serializable;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Singleton implements Serializable { // 实现Serializable接口

// 私有化成员
private static Singleton singleton = null;

// 私有化构造方法
private Singleton() {
    System.out.println("执行了几次构造方法????");
    if (Holder.INSTANCE != null) {
        throw new RuntimeException("不允许创建多个实例");            // 如果通过构造方法来创建对象则抛出异常
    }
}

// 共有访问方法
public static Singleton getSingleton() {
    return Holder.INSTANCE;
}

// 静态内部类
private static class Holder {
    private static final Singleton INSTANCE = new Singleton();
}

// 该对象被反序列化时返回的对象
private Object readResolve() {
    return Holder.INSTANCE;
}

}


* 再次运行反序列化测试代码,发现能保证单例;


##### 3) 探究反序列化源码


为什么在对象里面添加了readResolve()方法就可以保证对象的单例呢?这得从序列化的源码开始说起…


* 1)我们点开ObjectInputStream类的readObject()方法:



public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}

// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
    Object obj = readObject0(false);			// 核心方法
    handles.markDependency(outerHandle, passHandle);
    ClassNotFoundException ex = handles.lookupException(passHandle);
    if (ex != null) {
        throw ex;
    }
    if (depth == 0) {
        vlist.doCallbacks();
    }
    return obj;
} finally {
    passHandle = outerHandle;
    if (closed && depth == 0) {
        clear();
    }
}

}


* 2)点开readObject0()方法:



private Object readObject0(boolean unshared) throws IOException {
… 代码省略

    depth++;
totalObjectRefs++;
try {
    switch (tc) {
        case TC_NULL:
            return readNull();

        case TC_REFERENCE:
            return readHandle(unshared);

        case TC_CLASS:
            return readClass(unshared);

        case TC_CLASSDESC:
        case TC_PROXYCLASSDESC:
            return readClassDesc(unshared);

        case TC_STRING:
        case TC_LONGSTRING:
            return checkResolve(readString(unshared));

        case TC_ARRAY:
            return checkResolve(readArray(unshared));

        case TC_ENUM:
            return checkResolve(readEnum(unshared));

        case TC_OBJECT:
            return checkResolve(readOrdinaryObject(unshared));			// 反序列化对象时执行的方法(核心方法)

        case TC_EXCEPTION:
            IOException ex = readFatalException();
            throw new WriteAbortedException("writing aborted", ex);

            ... 代码省略
    }
} finally {
    depth--;
    bin.setBlockDataMode(oldMode);
}

}


* 3)打开readOrdinaryObject方法:



private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}

ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();

Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
    || cl == ObjectStreamClass.class) {
    throw new InvalidClassException("invalid class descriptor");
}

Object obj;
try {
    // 判断有没有无惨构造方法,如果是无惨构造方法就实例化对象
    obj = desc.isInstantiable() ? desc.newInstance() : null;				
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(
        desc.forClass().getName(),
        "unable to create instance").initCause(ex);
}

passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
    handles.markException(passHandle, resolveEx);
}

if (desc.isExternalizable()) {
    readExternalData((Externalizable) obj, desc);
} else {
    readSerialData(obj, desc);
}

handles.finish(passHandle);

if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())							// 核心代码
{
    Object rep = desc.invokeReadResolve(obj);						
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        // Filter the replacement object
        if (rep != null) {
            if (rep.getClass().isArray()) {
                filterCheck(rep.getClass(), Array.getLength(rep));
            } else {
                filterCheck(rep.getClass(), -1);
            }
        }
        handles.setObject(passHandle, obj = rep);
    }
}

return obj;

}


4)hasReadResolveMethod源码:



boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}


简单的判断一下readResolveMethod变量是否为空,那么readResolveMethod是在哪里被赋值的呢?


5)通过查看源码得知,readResolveMethod在ObjectStreamClass的自由构造方法中被赋值:



readResolveMethod = getInheritableMethod(cl, “readResolve”, null, Object.class);


6)getInheritableMethod源码如下:



private static Method getInheritableMethod(Class<?> cl, String name, Class<?>[] argTypes,
Class<?> returnType) { Method meth = null; Class<?> defCl = cl;
while (defCl != null) {
try {
meth = defCl.getDeclaredMethod(name, argTypes);
break;
} catch (NoSuchMethodException ex) {
defCl = defCl.getSuperclass();
}
}

if ((meth == null) || (meth.getReturnType() != returnType)) {
    return null;
}
meth.setAccessible(true);
int mods = meth.getModifiers();
if ((mods & (Modifier.STATIC | Modifier.ABSTRACT)) != 0) {
    return null;
} else if ((mods & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0) {
    return meth;
} else if ((mods & Modifier.PRIVATE) != 0) {
    return (cl == defCl) ? meth : null;
} else {
    return packageEquals(cl, defCl) ? meth : null;
}

}


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6uE8C9kA-1655988179502)(…/media/15.png)]


上面代码的逻辑其实就是通过反射找到一个readResolve方法,之后保存下来;赋值给readResolveMethod


7)再回到ObjectInputStream的readOrdinaryObject方法:



private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}

ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();

Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
    || cl == ObjectStreamClass.class) {
    throw new InvalidClassException("invalid class descriptor");
}

Object obj;
try {
    obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(
        desc.forClass().getName(),
        "unable to create instance").initCause(ex);
}

passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
    handles.markException(passHandle, resolveEx);
}

if (desc.isExternalizable()) {
    readExternalData((Externalizable) obj, desc);
} else {
    readSerialData(obj, desc);
}

handles.finish(passHandle);

if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);				// 最终返回的是desc.invokeReadResolve方法的返回值
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        // Filter the replacement object
        if (rep != null) {
            if (rep.getClass().isArray()) {
                filterCheck(rep.getClass(), Array.getLength(rep));
            } else {
                filterCheck(rep.getClass(), -1);
            }
        }
        handles.setObject(passHandle, obj = rep);
    }
}

return obj;

}


8)`desc.invokeReadResolve`源码如下:



Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null); // 将readResolveMethod封装的方法执行
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}


这个方法的逻辑就是将`readResolveMethod`封装的方法执行,`readResolveMethod`封装的方法正是我们之前查看的`readResolve`方法;也就是说,**最终反序列化返回的对象是这个对象readResolve方法的返回值;**


#### 4.1.6 枚举保证单例设计模式


##### 1)设计枚举单例类


* 枚举单例类:



package com.dfbz.demo10_枚举设计单例;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public enum Singleton {
INSTANCE;

public static Singleton getSingleton() {
    return INSTANCE;
}

}


* 测试枚举单例类:



package com.dfbz.demo10_枚举设计单例;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01 {
public static void main(String[] args) throws Exception {
Singleton s1 = Singleton.INSTANCE;

    // 测试反射
    Class<Singleton> clazz = Singleton.class;

// Singleton s3 = clazz.newInstance(); // 出现异常: java.lang.NoSuchMethodException

    // 测试序列化
    try (
            // 对象输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));

            // 对象输入流
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
    ) {
        // 写出对象
        oos.writeObject(s1);

        // 反序列化
        Singleton s2 = (Singleton) ois.readObject();

        System.out.println(s1 == s2);               // true,枚举能够保证对象在反序列化时的一致
    } catch (Exception e) {
        e.printStackTrace();
    }
}

}


##### 2)为什么枚举单例对象不会被破坏?


* 1)探究为什么反射不会破坏枚举单例对象:


在Enum类中定义着如下的构造方法:



protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}


意味着所有的枚举类都会有一个这样的构造方法(空参构造方法将会被擦除)


我们尝试反射其有参构造方法:



package com.dfbz.demo10_枚举设计单例;

import java.lang.reflect.Constructor;

/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02_探究反射破坏枚举单例对象 {
public static void main(String[] args) throws Exception{

    // 获取指定的构造器
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int.class);

    // 设置强制访问
    constructor.setAccessible(true);

    // 创建对象
    Singleton singleton = constructor.newInstance("xxx", 110);
}

}


运行程序出现如下错误:


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



> 
> Tips:在Java的反射机制中,不允许对枚举类进行反射;
> 
> 
> 


* 2)探究为什么反序列化不会破坏枚举单例对象?


关于反序列化的代码都在ObjectInputStream类的readEnum方法中:



private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}

ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
    throw new InvalidClassException("non-enum class: " + desc);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值