首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >聊一聊最难的设计模式 - 单例模式

聊一聊最难的设计模式 - 单例模式

作者头像
山海散人
发布于 2021-03-03 03:52:12
发布于 2021-03-03 03:52:12
31700
代码可运行
举报
文章被收录于专栏:山海散人技术山海散人技术
运行总次数:0
代码可运行

很多人上来肯定一脸懵逼,因为在你的印象中,单例模式实现起来还是很简单的。不要着急,慢慢往下看,你就知道为什么我说它最难了。

1. 基本概念

  • 单例模式是一种常用的创建型设计模式。单例模式保证类仅有一个实例,并提供一个全局访问点。

2. 适用场景

  • 想确保任何情况下都绝对只有一个实例。
  • 典型的场景有:windows 的任务管理器、windows 的回收站、线程池的设计等。

3. 单例模式的优缺点

优点
  • 内存中只有一个实例,减少了内存开销。
  • 可以避免对资源的多重占用。
  • 设置全局访问点,严格控制访问。
缺点
  • 没有接口,扩展困难。

4. 常见的实现模式

  • 懒汉式
  • 饿汉式

5. 先搞一个懒汉式的玩一玩

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LazySingleton {
    // 1. 私有对象
    private static LazySingleton lazySingleton = null;

    // 2. 构造方法私有化
    private LazySingleton() {}

    // 3. 设置全局访问点
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}
接下来,我们单线程测试
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MainTest {
    public static void main(String[] args) {
        LazySingleton instance = LazySingleton.getInstance();
        LazySingleton instance2 = LazySingleton.getInstance();
        System.out.println(instance == instance2);
    }
}
  • 测试代码及结果如上,一切看着毫无违和感。
那自然而然,我们考虑一下多线程如何呢。
  • 我们来创建一个线程类
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MyThread implements Runnable {
    @Override
    public void run() {
        LazySingleton instance = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + " " + instance);
    }
}
  • 然后修改我们的测试代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MainTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread());
        Thread t2 = new Thread(new MyThread());
        t1.start();
        t2.start();
        System.out.println("program end.");
    }
}
  • 我们通过 IDEA 自带的断点测试来测试多线程下的问题,我们在 LazySingleton 如下位置打上断点。(设置断点的 Suspend 为 Thread)
  • 我们通过 debug 方式启动测试代码,然后通过 IDEA 的工具窗口切换线程进行查看。(具体的 IDEA 调试多线程代码的方法可以通过各种途径学习,当然,也可以找我,我教你。虽然我也是略知皮毛。)
  • 此时会看到有 Thread-0 和 Thread-1 两个线程,此时两个线程都判断了 lazySingleton 为空,此时两个线程都会创建对象。
  • 将代码执行完,此时可以看到控制台打印的消息。
  • 很明显地,两个线程拿到的是不同的对象。也就说明了,我们如上的懒汉式代码不是线程安全的,在多线程下可能会创建出多个对象。
那接下来,我们就应该想办法处理这种情况了。
  • 通过在全局访问点添加 synchronized 关键字处理
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 3. 设置全局访问点
public synchronized static LazySingleton getInstance() {
    if (lazySingleton == null) {
        lazySingleton = new LazySingleton();
    }
    return lazySingleton;
}
如上问题是处理了,但是出现了新的问题,该方法访问时会加锁,导致访问效率降低,但是只要是判断和创建对象的时候加锁即可,大概率情况下,该对象已经创建出来,并发访问也是没有什么问题的。为了实现这个目的,我们又提出了“Double Check 双重检查方案”
  • 废话不多说,上代码。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;

    private LazyDoubleCheckSingleton() {}

    public static LazyDoubleCheckSingleton getInstance() {
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}
  • 此代码便实现了在大概率情况下,lazyDoubleCheckSingleton 已经不为空,也就不需要获取到锁,可以实现多线程并发访问。
但是如上代码还是有一些问题的,因为问题很难复现,也就不做演示。问题是由大名鼎鼎的“指令重排序”引起的。
  • 来大概说明一下原理,可能不是很准确,但是主要以理解这个问题为目的。
  • 其实创建对象(new LazyDoubleCheckSingleton())这个操作在底层我们可以看作三个步骤:
    • memory = allocate(); // 1:分配对象的内存空间
    • ctorInstance(memory); // 2:初始化对象
    • lazyDoubleCheckSingleton = memory; // 3:设置 lazyDoubleCheckSingleton 指向刚分配的内存地址
  • 针对这个问题,Java 语言规范中是有要求的,就是必须遵守 intra-thread semantics (线程内语义),保证重排序不会改变单线程内的程序执行结果。
  • 但是在上述例子中,2、3步骤可能会出现重排序,也就是可能出现,先指向内存地址,再初始化对象,此时,lazyDoubleCheckSingleton 不为空,但是对象还未初始化完成。问题也就出现了。并且此时重排序操作并不会违反 intra-thread semantics,因为在单线程的运行下,此类重排序是不会影响最终结果的。
上一个图来说明一下指令重排序引起的问题吧
  • 此时便会发生:线程0中对象未初始化完成,线程1就访问了对象。
那问题来了,也就该处理了。
  • 针对以上问题,我们处理思路其实有两种:
    • 不允许步骤 2、3 进行重排序。
    • 允许步骤 2、3 进行重排序,但是这个重排序过程不能让其他线程看到。
不允许步骤 2、3 进行重排序
  • 只需要对象添加 volatile 关键字即可。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;
  • 具体其中的原理,会在其他内容中进行分析,不是此次的重点。
允许步骤 2、3 进行重排序,但是这个重排序过程不能让其他线程看到。

基于静态内部类的解决方案

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class StaticInnerClassSingleton {
    // InnerClass 对象锁
    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    private StaticInnerClassSingleton() {}

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
}
到此为止,咱们的懒汉式先告一段落啊。。。丧心病狂呀,有木有。。。

6. 那咱们就再来玩玩饿汉式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class HungrySingleton {
    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }

    private HungrySingleton() {}

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}
  • 这个东西在多线程下就好点了,因为饿汉式是在类初始化的时候便把对象创建好了,所以也不需要判断对象是不是空,当然,在多线程下也就没那么多需要我们考虑的了。

7. 然后,然后,咱们再来看看序列化和反序列化的情况下,单例模式有没有什么问题呢。

  • 因序列化问题与懒汉式还是饿汉式实现无关,以下便以饿汉式代码为例展示。
饿汉式
  • 首先,我们的单例类实现序列化
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class HungrySingleton implements Serializable {
    // ...
}
  • 然后我们来写一个测试代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MainTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
        oos.writeObject(instance);

        File file = new File("test.txt");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));

        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 我们来看一下执行结果
  • 哈哈哈哈,瞬间窒息了,有木有。。。
针对上面问题,咱们来看一看源码,找一找原因啊。
  • 如下是跟踪源码的过程,我只做简单截图,有兴趣可自行研究(哈哈哈,或者你可以找我呀,我们一起研究)。
看到这儿,我感觉你应该也就知道了,desc.isInstantiable()方法返回了true,所以通过反射new了一个新的对象,导致读出的对象与写入的对象不是同一个对象。
那你一定想问我,那怎么处理呢,别着急啊,接着往下看。
  • 这个变量的初始化,可以直接通过查找看到。
这不就清楚了嘛,有readResolve()方法的时候,直接通过调用该方法返回了单例对象,那我们处理起来也就简单了,为我们的单例类添加一个方法即可。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private Object readResolve() {
    return hungrySingleton;
}
  • 然后重新直接测试代码,会出现如下结果。

8. 序列化和序列化的问题说完了,咱们再来看看反射的问题吧,毕竟反射我们用的还是很多的,通过反射去创建一个对象也是常用的操作。

该问题针对两种方式是不一样的,我们先来看看饿汉式的表现。
  • 我们来写个测试代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MainTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = HungrySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 看看运行结果
有一种五雷轰顶的感觉了没,别着急,别着急,咱们慢慢搞啊,虽然花点时间,但是能搞到很多东西的。
既然问题出来了,那怎么处理呢?其实处理也简单,因为反射是讲私有构造方法权限进行了开放,那我们在私有构造中添加判断即可。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private HungrySingleton() {
    if (hungrySingleton != null) {
        throw new RuntimeException("单例构造器禁止反射调用!");
    }
}
  • 再来运行我们的测试代码,可以看到会抛出以下异常。
接下来我们分析分析懒汉式
  • 与饿汉式添加同样的操作,也是避免不了反射的。
  • 假如先使用getInstance()方法获取对象,然后使用反射创建对象,是可以抛出异常的。
  • 但是当先使用反射创建对象,再通过getInstance()方法获取对象时,便可以获取到两个不同的对象,还是避免不了对单例模式的破坏。
最终的结论,懒汉式是无法防止反射攻击的。

9. 然后估计你就快晕了,你肯定想问,难道以后做一个单例都要考虑这么多问题嘛,也太墨迹了点吧。那咱们接下来就看看用枚举来实现单例的方法吧。

  • 该方法为Effective Java书中推荐的用法。
  • 该方法完美解决了序列化及反射对单例模式的破坏。
上代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public enum EnumInstance {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}
上面既然说了,完美解决了序列化及反射对单例模式的破坏,那咱们接下来就看看是如何解决的。
解决序列化对单例模式的破坏
  • 我们还是来看ObjectInputStream.readObject()方法
  • 可以看出是使用名称通过反射去获取到Enum,并没有创建新的对象,所以获取到的是同一个对象。
解决反射对单例模式的破坏
  • 来写一个测试代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MainTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = EnumInstance.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumInstance instance = EnumInstance.getInstance();
        EnumInstance newInstance = (EnumInstance) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 结果
  • 来看一下 java.lang.Enum 类,我们可以看到只有一个构造方法,且需要两个参数。
  • 那我们就来传入两个参数试一下。
  • 最终的结果
  • 我们来看一下原因啊,请看 constructor.newInstance() 方法
  • 发现其对 Enum 类型进行了处理,不允许通过反射创建 Enum 对象。
  • 至此我们也就明白了,为什么 Enum 单例可以完美防止序列化及反射对单例模式的破坏了。

OK 了,我们再来搞两个相关的东西

10. 我们来聊聊容器单例

  • 为了方便,使用 HashMap 来实现一个容器单例
直接走代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ContainerSingleton {
    private static Map<String, Object> singletonMap = new HashMap<>();

    private ContainerSingleton() {}

    public static void putInstance(String key, Object instance) {
        if (key != null && !"".equals(key) && instance != null) {
            if (!singletonMap.containsKey(key)) {
                singletonMap.put(key, instance);
            }
        }
    }

    public static Object getInstance(String key) {
        return singletonMap.get(key);
    }
}
针对上述代码的说明
  • 因其key 相同,所以最终获取到的是同一个对象。
  • 但是上述代码是线程不安全的。在多线程情况下,如果两个线程同时判断 if 条件成立,此时 t1 线程 put,t1 线程 get;然后 t2 线程 put ,t2 线程 get 时,t1 线程与 t2 线程获取到的对象是不同的。
  • 如果此时容器单例不使用 HashMap,而使用 HashTable 是可以实现线程安全的,但是从性能考虑,假如 get 请求多的情况下,HashTable 效率会非常低下。

11. 最后一个,我们来看看 ThreadLocal 线程单例怎么实现

定义一个线程单例类
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal =
            new ThreadLocal<ThreadLocalInstance>() {
                @Override
                protected ThreadLocalInstance initialValue() {
                    return new ThreadLocalInstance();
                }
            };

    private ThreadLocalInstance() {}

    public static ThreadLocalInstance getInstance() {
        return threadLocalInstanceThreadLocal.get();
    }
}
实现一个线程类做测试
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MyThread implements Runnable {
    @Override
    public void run() {
        ThreadLocalInstance instance = ThreadLocalInstance.getInstance();
        System.out.println(Thread.currentThread().getName() + " " + instance);
    }
}
写一个测试代码来测试一下
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MainTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.out.println(Thread.currentThread().getName() + " " + ThreadLocalInstance.getInstance());
        System.out.println(Thread.currentThread().getName() + " " + ThreadLocalInstance.getInstance());
        System.out.println(Thread.currentThread().getName() + " " + ThreadLocalInstance.getInstance());
        Thread t1 = new Thread(new MyThread());
        Thread t2 = new Thread(new MyThread());
        t1.start();
        t2.start();
        System.out.println("program end.");
    }
}
结果

我们今天的讨论到现在就结束了。今天主要讨论了入如下内容。

  • 基本的单例模式的实现:懒汉式和饿汉式。
  • 针对多线程下的单例模式线程安全的讨论。
  • 序列化和反序列化对单例模式的破坏。
  • 反射对单例模式的破坏。
  • Enum 枚举单例。
  • 单例容器。
  • ThreadLocal 线程单例。

朋友们,一起加油吧!!!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019/09/11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【设计模式-单例模式】
今天来说一下同样属于创建型模式的单例模式,相信这个模式普遍都清楚,因为平时在编码的时候都会进行相应的使用,我这边就当做日志记录一下。免得以后忘了还得去搜,我发现我记忆里非常差,很多东西很快就忘记了,年纪大了没办法。
Liusy
2020/09/01
5860
【设计模式-单例模式】
Java设计模式之单例模式
一般单例模式口诀:两私一公。 具体说就是私有构造方法、私有静态实例、公开的静态获取方法。
程裕强
2022/05/06
2760
Java设计模式之单例模式
单例模式的实现方式汇总
枚举是实现单例模式的最佳实践反射安全序列化/反序列化安全写法简单饿汉式public class HungryStaticSingleton { //先静态后动态 //先上,后下 //先属性后方法 private static final HungryStaticSingleton hungrySingleton; //装个B static { hungrySingleton = new HungryStaticSingleton(); } pri
在下是首席架构师
2022/08/18
2690
23种设计模式之单例模式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
暴躁的程序猿
2022/03/23
1850
23种设计模式之单例模式
23设计模式之 --------- 单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
默 语
2024/11/20
1070
23设计模式之 --------- 单例模式
一个单例模式,没必要这么卷吧
老猫的设计模式专栏已经偷偷发车了。不甘愿做crud boy?看了好几遍的设计模式还记不住?那就不要刻意记了,跟上老猫的步伐,在一个个有趣的职场故事中领悟设计模式的精髓。还等什么?赶紧上车吧
程序员老猫
2024/02/22
1760
一个单例模式,没必要这么卷吧
单例模式深入理解
最近去平安系面试时,遇到了个人技术领域认定的一大偶像吴大师(Cat作者),他随口问了个单例的问题,要求基于Java技术栈,给出几种单例的方案,并给出单元测试代码,最后要求谈谈单例模式最需要注意的问题时什么?我想想挺简单的,就是一个饿汉,一个懒汉模式,单元测试就一个判断NULL和2个Instance的比较就好。结果被大师劈头盖脸一顿数落,比如我写的懒汉单例(双锁),为什么使用volatile?还有别的更好的方式么?单元测试你不起多个线程,简单的比较有任何意义么?最后被定位为写代码不懂脑筋,仅仅就是照抄别人的成
用户1216676
2018/01/24
9520
三、单例模式详解
2、单例模式是非常经典的高频面试题,希望通过面试单例彰显技术深度,顺利拿到Offer的人群。
编程之心
2020/08/12
9430
三、单例模式详解
单例模式
背景:我们在实现单例模式的时候往往会忽略掉多线程的情况,就是写的代码在单线程的情况下是没问题的,但是一碰到多个线程的时候,由于代码没写好,就会引发很多问题,而且这些问题都是很隐蔽和很难排查的。
大学里的混子
2019/04/02
4610
单例设计模式
# 单例模式需要满足: 私有的构造函数 懒加载 线程安全 通过静态方法来访问实例 无法通过反射来实例化对象 无法通过反序列化来实例化对象 1. 饿汉模式 package com.futao.springbootdemo.design.pattern.gof.a.singleton; /** * 单例模式1-饿汉模式,即在类加载的时候就实例化对象。 * * @author futao * Created on 2018-12-25. */ public class EagerSingleton {
喜欢天文的pony站长
2020/06/29
3470
单例设计模式
摸鱼设计模式——单例模式
饿汉式单例,无论是否使用,都直接初始化。其缺点则是会浪费内存空间。因为假如整个实例都没有被使用,那么这个类依然会创建,这就白创建了。
摸鱼-Sitr
2021/01/04
6890
摸鱼设计模式——单例模式
我向面试官讲解了单例模式,他对我竖起了大拇指
单例模式相信大家都有所听闻,甚至也写过不少了,在面试中也是考得最多的其中一个设计模式,面试官常常会要求写出两种类型的单例模式并且解释其原理,废话不多说,我们开始学习如何很好地回答这一道面试题吧。
cxuan
2020/07/22
6050
我向面试官讲解了单例模式,他对我竖起了大拇指
单例模式详解
注意:synchronized 解决并发问题,但是因为lazyMan = new LazyMan();不是原子性操作(可以分割,见代码注释),可能发生指令重排序的问题,通过volatil来解决
崔笑颜
2020/06/28
6240
设计模式入门:单例模式
### UML类图 ![单例模式](http://upload-images.jianshu.io/upload_images/9709135-eba21220b6f018cd.jpg?imageMo
佛系贲八拉
2021/09/10
2320
设计模式入门:单例模式
设计模式——单例模式
关于单例模式,这是面试时最容易遇到的问题。当时以为很简单的内容,深挖一下,也可以关联出类加载、序列化等知识。
健程之道
2020/03/11
4270
一个单例模式,被问7个问题,难!
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。单例模式属于创建型模式,它提供了一种创建对象的最佳方式。
田维常
2022/04/19
8730
一个单例模式,被问7个问题,难!
万字总结之单例模式
这里不得不先吐槽下,尤其是接手原来的老项目,负责人已经溜了,你不得不上。哎,要是代码写的优雅,注释明了,那就是祖上烧高香了,得此优秀项目。那要是写的一团糟,代码耦合性强,牵一发而动全身,注释根本没有,变量名方法名不规范,甚至用拼音的,那真的分分钟想打人的心都有了。那读代码根本就靠猜好吗,完全考验你和对方的心有灵犀程度。哎,算了,小仙女不生气,要温柔(微笑脸)。
陈琛
2020/06/12
3860
万字总结之单例模式
单例模式的迭代式优化过程
在软件设计架构中,单例模式是最为常用的一种设计模式,所谓单例模式是指在创建某一个类的对象实例时该系统中有且仅有该类的一个实例,从而可以合理解决实例化对象的性能开销、资源分配等问题。从实现角度看,单例模式主要分为两种,一般称为饿汉式单例模式和懒汉式单例模式,以下逐一介绍
用户7506105
2021/08/09
3450
设计模式 | 单例模式及典型应用
单例是最常见的设计模式之一,实现的方式非常多,同时需要注意的问题也非常多。要内容:
小旋锋
2019/01/21
1K0
挑战一文搞懂带你搞懂单例模式,面试手撕双重检查锁定单例模式不害怕!
最近在刷牛客的时候,发现现在的面试官出笔试题都已经不局限在Hot100,大把大把的同学在面试的时候被考到了与设计模式相关的笔试题。
程序员牛肉
2024/11/21
4940
挑战一文搞懂带你搞懂单例模式,面试手撕双重检查锁定单例模式不害怕!
相关推荐
【设计模式-单例模式】
更多 >
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档