设计模式之单例模式

单例模式

单例模式是确保一个类在任何情况下都只有一个实例, 并自行实例化向整个系统提供一个全局的访问点.

一、概念

特征

  1. 只能有一个实例;
  2. 必须自己创建自己的唯一实例;
  3. 必须向整理系统提供这个实例;

单例模式分类

  • 饿汉式单例;
  • 懒汉式单例;
  • 注册式单例;
  • ThreadLocal 线程式单例;
  • 枚举单例;

实现步骤

  1. 私有构造方法, 防止多个实例的产生;
  2. 私有静态实例变量, 保证不被外面置空和唯一;
  3. 公共的静态访问方法, 提供自己创建好的实例;

优点

  1. 某些类创建比较频繁, 对于一些大型对象的创建是一笔很大的系统开销.
  2. 省去了 new 操作符, 降低了系统内存的使用频率, 减轻 GC 压力;
  3. 可以保证内存中只有一个实例, 减少了内存开销, 可以避免对资源的多重占用

类图

在这里插入图片描述

二、代码演示

饿汉式

  • 特点 : 在类加载的时候就立即初始化,并且创建单例对象. 绝对线程安全,在线程还没出现以前实例化了, 不可能存在访问安全问题, 并且没有加锁, 它的执行效率比较高. 但因为类加载的时候就初始化了, 不管用不用都会占用内存空间, 形成内存空间的浪费.
/**
 * @Author: CaoJun
 * @Description: 饿汉式单例模式
 * @Create: 2020-01-17 20:09
 **/
public class HungrySingleton implements Serializable {

    private static final long serialVersionUID = -8686933389819315943L;

    /**
     * 1. 私有静态实例变量,保证唯一和不被外面置空(饿汉式:一开始就实例化对象)
     */
    private static final HungrySingleton HUNGRY_SINGLETON_INSTANCE = new HungrySingleton();

    /**
     * 2. 私有化构造方法, 防止多个实例产生, 判空(防止反射破坏)
     */
    private HungrySingleton() {
        if (HUNGRY_SINGLETON_INSTANCE != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    /**
     * 3. 公有的静态访问方法, 向整个系统提供全局访问点
     */
    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON_INSTANCE;
    }

    /**
     * 4. 防止序列化: 如果该对象被用于序列化,可以保证对象在序列化前后保持一致
     */
    private Object readResolve() {
        return HUNGRY_SINGLETON_INSTANCE;
    }
}
  • 饿汉式静态代码块
/**
 * @Author: CaoJun
 * @Description: 饿汉式静态代码块单例模式
 * @Create: 2020-01-17 20:13
 **/
public class HungryStaticSingleton  implements Serializable {

    private static final long serialVersionUID = -4191127512435945546L;

    /**
     * 1. 私有静态实例变量,保证唯一和不被外面置空(饿汉式:一开始就实例化对象)
     */
    private static HungryStaticSingleton HUNGRY_STATIC_SINGLETON_INSTANCE = null;

    // 2. 静态代码块实例化
    static {
        HUNGRY_STATIC_SINGLETON_INSTANCE = new HungryStaticSingleton();
    }

    /**
     * 3. 私有化构造方法, 防止多个实例产生, 判空(防止反射破坏)
     */
    private HungryStaticSingleton() {
        if (HUNGRY_STATIC_SINGLETON_INSTANCE != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    /**
     * 4. 公有的静态访问方法, 向整个系统提供全局访问点
     */
    public HungryStaticSingleton getInstance() {
        return HUNGRY_STATIC_SINGLETON_INSTANCE;
    }

     /**
     * 5. 防止序列化破坏: 如果该对象被用于序列化,可以保证对象在序列化前后保持一致
     */
    private Object readResolve() {
        return HUNGRY_STATIC_SINGLETON_INSTANCE;
    }
}

懒汉式

  • 特点: 被外部类调用的时候才会加载创建实例, 但是会有线程安全问题, 需要使用 synchronized 加锁, 这样就会有性能上的问题
/**
 * @Author: CaoJun
 * @Description: 懒汉式单例模式
 * @Create: 2020-01-17 20:16
 **/
public class LazySingleton implements Serializable {

    private static final long serialVersionUID = 8580274347085357039L;

    /**
     * 1. 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载
     */
    private static LazySingleton LAZY_SIMPLE_SINGLETON_INSTANCE = null;

    /**
     * 2. 私有化构造方法, 防止多个实例产生, 判空(防止反射破坏)
     */
    private LazySingleton() {
        if (LAZY_SIMPLE_SINGLETON_INSTANCE != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    /**
     * 3. 公有的静态访问方法, 创建实例, 向整个系统提供全局访问点
     */
    public static LazySingleton getInstance() {
        // 提高效率
        if (LAZY_SIMPLE_SINGLETON_INSTANCE == null) {
            // 保证线程安全
            synchronized (LazySingleton.class) {
                if (LAZY_SIMPLE_SINGLETON_INSTANCE == null) {
                    LAZY_SIMPLE_SINGLETON_INSTANCE = new LazySingleton();
                }
            }
        }
        return LAZY_SIMPLE_SINGLETON_INSTANCE;
    }

    /**
     * 4. 防止序列化: 如果该对象被用于序列化,可以保证对象在序列化前后保持一致
     */
    private Object readResolve() {
        return LAZY_SIMPLE_SINGLETON_INSTANCE;
    }
}
  • 懒汉式单例测试
/**
 * @Author: CaoJun
 * @Description: 
 * @Create: 2020-01-17 22:04
 **/
public class ExecutorThread implements Runnable {

    public void run() {
        LazySingleton singleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + singleton);
    }
}

/**
 * @Author: CaoJun
 * @Description: LazySingleton 测试
 * @Create: 2020-01-17 22:03
 **/
public class LazySingletonTest {

    public static void main(String[] args) {

        Thread t1 = new Thread(new ExecutorThread());
        Thread t2 = new Thread(new ExecutorThread());
        t1.start();
        t2.start();

        System.out.println("End");
    }
}
  • 懒汉式静态内部类
    • 特点: 兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
/**
 * @Author: CaoJun
 * @Description: 懒汉式内部类单例模式
 * @Create: 2020-01-17 20:21
 **/
public class LazyInnerClassSingleton implements Serializable {


    private static final long serialVersionUID = -7388698081929929093L;

    /**
     * 1. 私有化构造方法, 防止多个实例产生, 判空(防止反射破坏)
     */
    private LazyInnerClassSingleton() {
        if (LazyHolder.LAZY_INNER_CLASS_SINGLETON != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    /**
     * 2. 向整个系统提供全局访问点
     */
    public static LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY_INNER_CLASS_SINGLETON;
    }

    /**
     * 3. 此处使用一个内部类来维护单例
     */
    private static class LazyHolder {
        private static LazyInnerClassSingleton LAZY_INNER_CLASS_SINGLETON = new LazyInnerClassSingleton();
    }

    /**
     * 4. 防止序列化破坏: 如果该对象被用于序列化,可以保证对象在序列化前后保持一致
     */
    private Object readResolve() {
        return getInstance();
    }
}

枚举式

  • 特点: 防止反射和序列化破坏
/**
 * @Author: CaoJun
 * @Description: 注册式枚举单例模式
 * @Create: 2020-01-17 20:30
 **/
public enum EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

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

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

容器缓存式

  • 特点: 适用于创建实例非常多的情况,便于管理。但是,是非线程安全的
/**
 * @Author: CaoJun
 * @Description: 容器缓存方式单例模式
 * @Create: 2020-01-17 23:15
 **/
public class ContainerSingleton implements Serializable {

    private static final long serialVersionUID = -7388698631929929093L;
    
    /**
     * 1. 私有化构造方法, 防止多个实例产生, 判空(防止反射破坏)
     */
    private ContainerSingleton() {}
    
    /**
     * 2. 创建 Map 容器
     */
    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    /**
     * 3. 向整个系统提供全局访问点
     */
    public static Object getBean(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
    
    /**
     * 4. 防止序列化破坏: 如果该对象被用于序列化,可以保证对象在序列化前后保持一致
     */
    private Object readResolve() {
        return getInstance();
    }
}

ThreadLocal 线程

  • 特点: ThreadLocal 是将所有的对象全部都放在 ThreadLocalMap 中, 为每一个线程都提供一个对象, 实际上是以空间换时间来实现线程间的隔离的.
/**
 * @Author: CaoJun
 * @Description: ThreadLocal 线程单例模式
 * @Create: 2020-01-17 20:32
 **/
public class ThreadLocalSingleton {

    /**
     * 1. 使用 ThreadLocal 线程方式创建私有静态实例变量,保证唯一和不被外面置空
     */
    private static final ThreadLocal<ThreadLocalSingleton> SINGLETON_THREAD_LOCAL = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };

    /**
     * 2. 私有化构造方法, 防止多个实例产生, 判空(防止反射破坏)
     */
    private ThreadLocalSingleton() {
        if (SINGLETON_THREAD_LOCAL.get() != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    /**
     * 3. 公有的静态访问方法, 向整个系统提供全局访问点
     */
    public static ThreadLocalSingleton getInstance() {
        return SINGLETON_THREAD_LOCAL.get();
    }
    
     /**
     * 4. 防止序列化破坏: 如果该对象被用于序列化,可以保证对象在序列化前后保持一致
     */
    private Object readResolve() {
        return getInstance();
    }
}

三、使用 Idea 多线程调试单例模式

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、为什么加上 readResolve(); 方法可以防止序列化破坏?

  • 当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当 于破坏了单例. 加上 readResolve(); 方法可以防止序列化.
  • 源码解释
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * @author CaoJun
 * @Description: 序列化破坏单例模式
 * @Create: 2020-01-17 22:36
 */
public class LazySingletonTest {
    public static void main(String[] args) {
        LazySingleton s1 = null;
        LazySingleton s2 = LazySingleton.getInstance();
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tmp.obj"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("tmp.obj"));) {

            oos.writeObject(s2);
            oos.flush();
            // 进 入 ObjectInputStream 类的 readObject()方法
            s1 = (LazySingleton) ois.readObject();

            System.out.println(s1 + "\r\n" + s2 + "\r\n" + (s1 == s2));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 发现在 readObject 中又调用了我们重写的 readObject0() 方法, 进入 readObject0(); 方法
/**
 * 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();
            }
        }
    }
  • 看到 TC_OBJECTD 中判断,调用了 ObjectInputStream#readOrdinaryObject() 方法
/**
 * ObjectInputStream#readObject0()
 */
private Object readObject0(boolean unshared) throws IOException {
    // ......
    case TC_OBJECT:
    return checkResolve(readOrdinaryObject(unshared));
    // ......
}
  • 发现调用了 ObjectStreamClass#isInstantiable() 方法
/**
 * ObjectStreamClass#readOrdinaryObject()
 */
private Object readOrdinaryObject(boolean unshared)
    // ... 
    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);
    }
    // ...
    if (obj != null &&
        // 调用了 hasReadResolveMethod() 方法
        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;
}
  • 判断一下构造方法是否为空,构造方法不为空就返回 true
/**
 * ObjectStreamClass#isInstantiable()
 */
boolean isInstantiable() { 
    requireInitialized(); 
    return (cons != null); 
}
  • 判断构造方法是否存在之后,又调用了 hasReadResolveMethod() 方法, 判断是否为空,不为空就返回 true
boolean hasReadResolveMethod() { 
    requireInitialized(); 
    return (readResolveMethod != null); 
}
  • 通过全局搜索, 在ObjectStreamClass 中找到 readResolve 的赋值, 通过反射找到一个无参的 readResolve() 方法,并且保存下来
/**
 * ObjectStreamClass#ObjectStreamClass()
 */
private ObjectStreamClass(final Class<?> cl) {
    // ...
    readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
    // ...
}
  • invokeReadResolve() 方法中用反射调用了 readResolveMethod()方法, 通过 JDK 源码分析我们可以看出,虽然增加 readResolve() 方法返回实例,解决了单例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已, 该对象会被 GC 回收.
/**
 * ObjectStreamClass#invokeReadResolve()
 */
Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException
{
    requireInitialized();
    if (readResolveMethod != null) {
        try {
            // 
            return readResolveMethod.invoke(obj, (Object[]) null);
        } 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();
    }
}

五、为什么枚举单例模式能够防止序列化和反射破坏?

  1. 下载 jad 工具, 解压后配置好环境变量, 就可以使用命令行调用了;
  2. 找到工程所在的 class 目录,复制 EnumSingleton.class 所在的路径
在这里插入图片描述
  1. 使用命令行, 输入命令 jad 后面输入复制好的路径,在 class 目录下会多一个 EnumSingleton.jad 文件。打开 EnumSingleton.jad 文件, 有如下静态代码块: 枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现
static {
    INSTANCE = new EnumSingleton("INSTANCE", 0); 
    $VALUES = (new EnumSingleton[] { INSTANCE }); 
}
  1. 关于序列化的代码: 在 readObject0() 中调用了 readEnum() 方法,来看 readEnum() 中代码实现
/**
 * ObjectInputStream#readObject()
 */
private Object readObject0(boolean unshared) throws IOException { 
    // ... 
    case TC_ENUM: 
        return checkResolve(readEnum(unshared)); ... 
}
  1. 发现枚举类型其实通过类名和 Class对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次
/**
 * 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);
        }

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

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }
  1. 运行枚举类型的单例测试, 发现报出 java.lang.NoSuchMethodException 异常, 打开 java.lang.Enum 的源码代码,查看它的构造方法,只有一个 protected 的构造方法
/**
 * java.lang.Enum
 */
protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}
  1. 测试
/**
 * @Author: CaoJun
 * @Description:
 * @Create: 2020-01-17 23:10
 **/
public class EnumTest {
    public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
            c.setAccessible(true);
            EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("Tom", 666);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 执行结果: 告诉我们不能用反射来创建枚举类型
在这里插入图片描述
  1. 进入 JDK 源码中的 Constructor#newInstance() 方法中做了强制性的判断,如果修饰符是Modifier.ENUM 枚举类型, 直接抛出异常
/**
 * Constructor#newInstance()
 */
public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容