单例模式在JDK以及Spring源码中如何进行串联

创建型模式

目录

1、单例模式

1.1 单例模式UML图

1.2 日常生活中看单例模式

1.3 使用场景

1.4 具体例子

1.4.1 背景

1.4.2 网站计数的单例实现

2、单例模式在源码中的应用

2.1 JDK源码中单例模式

2.2 Spring源码中单例模式

3、单例模式优缺点与场景

3.1 优点

3.2 缺点

3.3 使用注意事项

3.4 适用场景

3.5 应用场景举例 

4、实现单利模式的原则和过程


1、单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

1.1 单例模式UML图

单例模式的 UML 图

1.2 日常生活中看单例模式

  • 1、一个班级只有一个班主任。
  • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
  • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

1.3 使用场景

  • 1、要求生产唯一序列号。
  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

1.4 具体例子

1.4.1 背景

  • 在企业网站后台系统中,一般会将网站统计单元进行独立设计,比如登录人数的统计、IP数量的计数等。在这类需要完成全局统计的过程中,就会用到单例模式即整个系统只需要拥有一个计数的全局对象。
  • 在网站登录这个高并发场景下,由这个全局对象负责统计当前网站的登录人数、IP等,即节约了网站服务器的资源,又能保证计数的准确性。

在这里插入图片描述

1.4.2 网站计数的单例实现

实现单例模式有多种写法,这里我们只列举其中最常用的三种实现方式,且考虑到网站登录高并发场景下,将重点关注多线程环境下的安全问题。

在这里插入图片描述

登录线程的实现:我们先创建一个登录线程类,用于登录及登录成功后调用单例对象进行计数。

/**
 * 单例模式的应用--登录线程
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class Login implements Runnable {
	// 登录名称
    private String loginName;

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    @Override
    public void run() {
		// TODO 
		// 登录成功后调用单例对象进行计数
    }
}

主程序的实现:编写一个主程序,利用多线程技术模拟10个用户并发登录,完成登录后输出登录人次计数。

/**
 * 单例模式--主程序
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class App {
    public final static int num = 10;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];

        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
            threads[i] = new Thread(login);
            threads[i].start();
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }

		// TODO
		// 调用单例对象输出登录人数统计
}

1.4.2.1 饿汉模式

  • 在程序启动之初就进行创建( 不管三七二十一,先创建出来再说)。
  • 天生的线程安全。
  • 无论程序中是否用到该单例类都会存在。
/**
 * 饿汉式单例模式
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class SimpleSingleton implements Serializable {
    // 单例对象
    private static final SimpleSingleton APP_INSTANCE = new SimpleSingleton();
    // 计数器
    private AtomicLong count = new AtomicLong(0);

    // 单例模式必须保证默认构造方法为私有类型
    private SimpleSingleton() {
    }

    public static SimpleSingleton getInstance() {
        return APP_INSTANCE;
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }

}

我们将饿汉模式的单例对象加入进登录线程及主程序中进行测试:

/**
 * 单例模式的应用--登录线程
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class Login implements Runnable {
    // 登录名称
    private String loginName;

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    @Override
    public void run() {
    	// 饿汉式单例
        SimpleSingleton simpleSingleton=  SimpleSingleton.getInstance();
        simpleSingleton.setCount();
        System.out.println(getLoginName()+"登录成功:"+simpleSingleton.toString());
    }

}

/**
 * 单例模式--主程序
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class App {
    public final static int num = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
            threads[i] = new Thread(login);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println("网站共有"+SimpleSingleton.getInstance().getCount()+"个用户登录");

    }
}

输出如下:
10个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式是有效的。

在这里插入图片描述

1.4.2.2 懒汉模式

  • 在初始化时只进行定义。
  • 只有在程序中调用了该单例类,才会完成实例化( 没人动我,我才懒得动)。
  • 需通过线程同步技术才能保证线程安全。

我们先看下未使用线程同步技术的例子:

/**
 * 懒汉式单例模式--未应用线程同步技术
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class LazySingleton {
    // 单例对象
    private static LazySingleton APP_INSTANCE;
    // 计数器
    private AtomicLong count = new AtomicLong(0);

    // 单例模式必须保证默认构造方法为私有类型
    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (APP_INSTANCE == null) {
            APP_INSTANCE = new LazySingleton();
        }
        return APP_INSTANCE;
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }

  }
/**
 * 单例模式的应用--登录线程
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class Login implements Runnable {
   
	....
    @Override
    public void run() {
		// 饿汉式单例
        LazySingleton lazySingleton =LazySingleton.getInstance();
        lazySingleton.setCount();
        System.out.println(getLoginName()+"登录成功:"+lazySingleton);
    }

}

/**
 * 单例模式--主程序-
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class App {
    public final static int num = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
            threads[i] = new Thread(login);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println("网站共有" + LazySingleton.getInstance().getCount() + "个用户登录");
    }
}

输出结果:
10个线程并发登录过程中,获取到了四个对象引用地址,该单例模式失效了。

在这里插入图片描述

对代码进行分析:

// 未使用线程同步
public static LazySingleton getInstance() {
		// 在多个线程并发时,可能会有多个线程同时进入 if 语句,导致产生多个实例
        if (APP_INSTANCE == null) {
            APP_INSTANCE = new LazySingleton();
        }
        return APP_INSTANCE;
    }

我们使用线程同步技术对懒汉式模式进行改进:

/**
 * 懒汉式单例模式
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class LazySingleton {
    // 单例对象 ,加入volatile关键字进行修饰
    private static volatile LazySingleton APP_INSTANCE;
    // 计数器
    private AtomicLong count = new AtomicLong(0);

    // 单例模式必须保证默认构造方法为私有类型
    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (APP_INSTANCE == null) {
            // 对类进行加锁,并进行双重检查
            synchronized (LazySingleton.class) {
                if (APP_INSTANCE == null) {
                    APP_INSTANCE = new LazySingleton();
                }
            }
        }
        return APP_INSTANCE;
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }

  }

再测试运行:
10个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式有效了。

在这里插入图片描述

1.4.2.3 枚举类实现单例模式

《Effective Java》 推荐使用枚举的方式解决单例模式。这种方式解决了最主要的;线程安全、自由串行化、单一实例。

/**
 * 利用枚举类实现单例模式
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public enum EnumSingleton implements Serializable {
    // 单例对象
    APP_INSTANCE;
    // 计数器
    private AtomicLong count = new AtomicLong(0);

    // 单例模式必须保证默认构造方法为私有类型
    private EnumSingleton() {
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }
    
}
/**
 * 单例模式的应用--登录线程
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class Login implements Runnable {
    ...
    @Override
    public void run() {
         EnumSingleton enumSingleton = EnumSingleton.APP_INSTANCE;
         enumSingleton.setCount();
        System.out.println(getLoginName()+"登录成功:"+enumSingleton.toString());

    }
}

/**
 * 单例模式--主程序
 *
 * @author zhuhuix
 * @date 2020-06-01
 */
public class App {
    public final static int num = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
            threads[i] = new Thread(login);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
         System.out.println("网站共有"+EnumSingleton.APP_INSTANCE.getCount()+"个用户登录");

    }
}

输出如下:
10个线程并发登录过程中,该单例模式是有效的。

在这里插入图片描述

2、单例模式在源码中的应用

2.1 JDK源码中单例模式

java.lang.Runtime使用了单例模式的饿汉式,源码如下:

在这里插入图片描述

2.2 Spring源码中单例模式

在 Spring 依赖注入Bean实例默认是单例的,我们由此展开。bean 可以被定义为两种模式:prototype(多例)和 singleton(单例)。

  • singleton(单例):只有一个共享的实例存在,所有对这个 bean 的请求都会返回唯一的实例。
  • prototype(多例):对这个 bean 的每次请求都会创建一个新的 bean 实例,类似于 new。

实例:配置文件内容如下:

<bean id="singleton" class="java.util.Date" scope="singleton"></bean>
<bean id="prototype" class="java.util.Date" scope="prototype"></bean>

Spring 中加载单例的过程都是在 BeanFactory 的 getBean() 方法中被定义的,其默认的功能在 AbstractBeanFactory 类中实现,主要包含两个功能。

  1. 从缓存中获取单例 Bean。
  2. Bean 的实例中获取对象。

getBean() 方法最终会调用 AbstractBeanFactory 的 doGetBean() 方法,源码如下。

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
                          @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    //对传入的beanName稍作修改,防止有一些非法字段,然后提取Bean的Name
    final String beanName = transformedBeanName(name);
    Object bean;
    //直接从缓存中获取单例工厂中的objectFactory单例
    Object sharedInstance = getsingleton(beanName);
    if (sharedInstance != null && args == null) {
        if (logger.isDebugEnabled()) {
            if (isSingletonCurrentlyInCreation(beanName)) {
                logger.debug("Returning eagerly cached instance of singleton bean '" +
                        beanName + "' that is not fully initialized yet - a consequence of a circular reference");
            } else {
            }
        }
        //返回对应的实例,从 Bean实例中获取对象
        bean = getObjectForBeanInstance(sharedInstance,name,beanName, null);
    } else {
        ...
    }
    ...
}

getBean() 方法不仅处理单例对象的逻辑,还处理原型对象的逻辑。继续看 getSingleton() 方法的代码实现。
getSingleton() 的工作流程:singletonObjects-->earlySingletonObjects-->singletonFactories-->创建单例实例

/**
* 单例对象的缓存
*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    //首先通过名字查找这个Bean是否存在
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            //查看缓存中是否存在这个Bean
            singletonObject = this.earlySingletonObjects.get(beanName);
            //如果这个时候的Bean实例还为空并且允许懒加载
            if (singletonObjects == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

在上面代码片段中,synchronized(this.singletonObjects) 是关键,但是前提条件 isSingletonCurrentlyInCreation 的返回值也是 true,也就是这个 Bean 正在被创建。因此,第一次调用 doGetBean() 的时候,getSingleton() 基本上都是返回 null,所以会继续执行 doGetBean() 方法中后面的逻辑。

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
            @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    // 获取beanDefinition
    final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                checkMergedBeanDefinition(mbd, beanName, args);
                // Guarantee initialization of beans that the current bean depends on.
                String[] dependsOn = mbd.getDependsOn();
                if (dependsOn != null) {
                    for (String dep : dependsOn) {
                        if (isDependent(beanName, dep)) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                    "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
                        }
                        registerDependentBean(dep, beanName);
                        try {
                            getBean(dep);
                        }
                        catch (NoSuchBeanDefinitionException ex) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                    "'" + beanName + "' depends on missing bean '" + dep + "'", ex);
                        }
                    }
                }
                // Create bean instance.
                if (mbd.isSingleton()) {
                    sharedInstance = getSingleton(beanName, () -> {
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            // Explicitly remove instance from singleton cache: It might have been put there
                            // eagerly by the creation process, to allow for circular reference resolution.
                            // Also remove any beans that received a temporary reference to the bean.
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                }
            }
            ...
        }
}

可以看到,在 BeanFactory 中,从 XML 中解析出来的相关配置信息被放在 BeanDefinitionMap 中,通过这个 Map 获取 RootBeanDefinition,然后执行判断语句if(mbd.isSingleton())。如果是单例的,则接着调用 getSingleton() 的重载方法,传入 mbd 参数。当从缓存中加载单例对象时,会把当前的单例对象在singletonObjects 中存放一份,这样可以保证在调用 getBean() 方法的时候,singletonObjects 中永远只有一个实例,在获取对象时才会给它分配内存,既保证了内存高效利用,又是线程安全的。

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
    // 直接从缓存中获取单例Bean
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            if (this.singletonsCurrentlyInDestruction) {
                throw new BeanCreationNotAllowedException(beanName,
                        "Singleton bean creation not allowed while singletons of this factory are in destruction " +
                        "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
            }
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet<>();
            }
            try {
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            catch (IllegalStateException ex) {
                // Has the singleton object implicitly appeared in the meantime ->
                // if yes, proceed with it since the exception indicates that state.
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    throw ex;
                }
            }
            catch (BeanCreationException ex) {
                if (recordSuppressedExceptions) {
                    for (Exception suppressedException : this.suppressedExceptions) {
                        ex.addRelatedCause(suppressedException);
                    }
                }
                throw ex;
            }
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                // 在singletonObject中添加要加载的单例
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

如此一来,当下次需要这个单例 Bean 时,可以直接从缓存中获取。在 Spring 中创建单例的过程虽然有点绕,但是逻辑非常清楚,就是将需要的对象放在 Map 中,下次需要的时候直接从 Map 中获取即可。

3、单例模式优缺点与场景

3.1 优点

 

  1.   在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例 
  2.   单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。 
  3.   提供了对唯一实例的受控访问。 
  4.   由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。 
  5.   允许可变数目的实例。 
  6.   避免对共享资源的多重占用。 

3.2 缺点

  1.     不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。 
  2.     由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。 
  3.     单例类的职责过重,在一定程度上违背了“单一职责原则”。 
  4.     滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。 

3.3 使用注意事项

  •     1.使用时不能用反射模式创建单例,否则会实例化一个新的对象 
  •     2.使用懒单例模式时注意线程安全问题 
  •     3.饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式) 

3.4 适用场景

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如: 

  •     1.需要频繁实例化然后销毁的对象。 
  •     2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 
  •     3.有状态的工具类对象。 
  •     4.频繁访问数据库或文件的对象。 

以下都是单例模式的经典使用场景: 

  •     1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。 
  •     2.控制资源的情况下,方便资源之间的互相通信。如线程池等。 

3.5 应用场景举例 

  •     1.外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件 
  •     2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~ 
  •     3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。 
  •     4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。 
  •     5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。 
  •     6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。 
  •     7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。 
  •     8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。 
  •     9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。 
  •     10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例. 

4、实现单利模式的原则和过程

  •     1.单例模式:确保一个类只有一个实例,自行实例化并向系统提供这个实例 
  •     2.单例模式分类:饿单例模式(类加载时实例化一个对象给自己的引用),懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)(java中饿单例模式性能优于懒单例模式,c++中一般使用懒单例模式) 
  •     3.单例模式要素: 
  •         a.私有构造方法 
  •         b.私有静态引用指向自己实例 
  •         c.以自己实例为返回值的公有静态方法 

参考文章:

https://www.cnblogs.com/zhuhuix/p/13030646.html

http://m.biancheng.net/view/8378.html

https://www.cnblogs.com/damsoft/p/6105122.html

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值