23种设计模式(二)单例模式

约定不等于承诺〃 2022-12-28 14:10 343阅读 0赞

单例模式

优点:
在内存中只有一个实例,减少了内存的开销
可以避免对资源的多重占用
缺点:
没有接口,无法扩展

必须点

私有构造器,线程安全的,序列化与反序列化对单利的破坏,反射攻击

懒汉式单例模式

  1. public class LazySingleton {
  2. private static LazySingleton lazySingleton=null;
  3. private LazySingleton(){
  4. }
  5. public static LazySingleton getInstance(){
  6. if(!Optional.ofNullable(lazySingleton).isPresent()){
  7. lazySingleton=new LazySingleton();
  8. }
  9. return lazySingleton;
  10. }
  11. }

Test:

  1. public class Test {
  2. public static void main(String[] args) {
  3. LazySingleton instance = LazySingleton.getInstance();
  4. System.out.println(instance);
  5. }
  6. }

由上代码:简单的单利模式,就产生了
单例模式是线程安全的吗? 答案:并不是,如下一一为大家解答,如何解决线程安全问题,接下来用多线程debug模式
创建线程类:

  1. public class T implements Runnable {
  2. @Override
  3. public void run() {
  4. LazySingleton lazySingleton = LazySingleton.getInstance();
  5. System.out.println(Thread.currentThread().getName()+lazySingleton);
  6. }
  7. }

Test类:

  1. public class Test {
  2. public static void main(String[] args) {
  3. Thread thread = new Thread(new T());
  4. Thread thread1 = new Thread(new T());
  5. thread.start();
  6. thread1.start();
  7. System.out.println("END");
  8. }
  9. }

如上代码:进行验证
在这里插入图片描述
竟然两个对象不一致,接下来给大家使用多线程debug讲解:
他的运行流程:
在这里插入图片描述
如上图:三个线程,我只创建了两个,为什么会有三个why?因为一个主线程,main
注意看这两个:
Thread-0
Thread-1
我先执行Thread-0 ,将线程0卡在这个地方,我再去执行Thread-1
在这里插入图片描述
Thread-1 也卡在这里,这样就会出现问题 线程1线程2同时进来覆盖其对象,我先将线程1执行完毕,在将线程2执行完毕
在这里插入图片描述
放行后的结果
在这里插入图片描述
所以说是不安全的,我如何将它变成线程安全的哪?加上 synchronized如果加在静态方法上 他锁的是.class这个类,加上

  1. public class LazySingleton {
  2. private static LazySingleton lazySingleton=null;
  3. private LazySingleton(){
  4. }
  5. public synchronized static LazySingleton getInstance(){
  6. if(!Optional.ofNullable(lazySingleton).isPresent()){
  7. lazySingleton=new LazySingleton();
  8. }
  9. return lazySingleton;
  10. }
  11. }

按照如上操作继续操作,就会出现阻塞,线程1进不去,直到线程0执行完毕,线程1才可以进入,这样保证了单利模式的线程安全性,synchronized加上会影响性能,还有一种能解决单利模式线程安全,大家静静的看着
在这里插入图片描述
双重检查:
双重检查的话没有什么问题,但是多线程的情况下,有可能出现指令重排序
1.给这个对象分配内存
2.初始化这个对象
3.指向lazyDoubleCheckSingleton 这个内存地址
4.初次访问对象
为什么会出现这种状况? 有可能2和3的位置颠倒,请看下面的图
有可能出现指令重排序 单线程内的指令重排序没有什么问题 多线程

  1. public static LazyDoubleCheckSingleton getInstance(){
  2. if(!Optional.ofNullable(lazyDoubleCheckSingleton).isPresent()){
  3. //双重检查 减少线程开销
  4. synchronized (LazyDoubleCheckSingleton.class){
  5. if(!Optional.ofNullable(lazyDoubleCheckSingleton).isPresent()){
  6. lazyDoubleCheckSingleton =new LazyDoubleCheckSingleton();
  7. }
  8. }
  9. }
  10. return lazyDoubleCheckSingleton;
  11. }

单线程下线程重排序,不会造成太大的影响,但是多线程 请看第二个图
在这里插入图片描述
如果两个线程,0线程已经进去,然后instance是否为null,然后进行线程1第一次访问对象,线程与线程之间不可见的,内存可见性,会出现异常情况,我们如何解决这种情况?
在这里插入图片描述
解决多线程下的指令重排序:volatile 这样就完成了,线程的可见性,内存的可见性,线程这块先不细讲,只是先公布答案,先不说原理,因为涉及太多计算机原理,汇编等,关注我后续单独讲解线程原理,满满的干干货!!!!

  1. public class LazyDoubleCheckSingleton {
  2. //保证线程不会重排序,线程可见性 内存的可见性
  3. private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton=null;
  4. private LazyDoubleCheckSingleton(){
  5. }
  6. public static LazyDoubleCheckSingleton getInstance(){
  7. if(!Optional.ofNullable(lazyDoubleCheckSingleton).isPresent()){
  8. //双重检查 减少线程开销
  9. synchronized (LazyDoubleCheckSingleton.class){
  10. if(!Optional.ofNullable(lazyDoubleCheckSingleton).isPresent()){
  11. //1.给这个对象分配内存
  12. //2.初始化这个对象
  13. //3.指向lazyDoubleCheckSingleton 这个内存地址
  14. //有可能出现指令重排序 单线程内的指令重排序没有什么问题 多线程
  15. lazyDoubleCheckSingleton =new LazyDoubleCheckSingleton();
  16. }
  17. }
  18. }
  19. return lazyDoubleCheckSingleton;
  20. }
  21. }

静态内部类的单例模式:
先看图 多线程情况下 线程0或者线程1 这两个有一个会获得Class对象的初始化锁,例如线程0抢到了锁,线程1是卡拿不到指令重排序的,线程1是卡在绿色的部分的,就算指令重排序了,也没有问题
在这里插入图片描述
代码如下:
别忘了私有构造器哦,为了防止别人new出来这个对象

  1. /** * 静态内部类 单例模式 */
  2. public class StaticInnerClassSingleton {
  3. private static class InnerClass{
  4. private static StaticInnerClassSingleton staticInnerClassSingleton=new StaticInnerClassSingleton();
  5. }
  6. public StaticInnerClassSingleton getInstance(){
  7. return InnerClass.staticInnerClassSingleton;
  8. }
  9. //私有构造器 为了不让别人new 出来
  10. private StaticInnerClassSingleton() {
  11. }
  12. }

执行结果:
都是为了做延迟加载

  1. new Thread(()->{
  2. StaticInnerClassSingleton lazySingleton = StaticInnerClassSingleton.getInstance();
  3. System.out.println(Thread.currentThread().getName()+lazySingleton);
  4. }).start();
  5. new Thread(()->{
  6. StaticInnerClassSingleton lazySingleton = StaticInnerClassSingleton.getInstance();
  7. System.out.println(Thread.currentThread().getName()+lazySingleton);
  8. }).start();

在这里插入图片描述

饿汉式单利模式:

类加载的时候该对象会被创建,如果该类初始化,从来没有被使用过,造成内存的浪费
创建的两种方式
第一种: 静态成员变量 类初始化的时候被创建

  1. public class HungrySingleton {
  2. private final static HungrySingleton hungrySingleton=new HungrySingleton();
  3. public static HungrySingleton getInstance(){
  4. return hungrySingleton;
  5. }
  6. }

第二种: 通过静态代码块进行构建

  1. public class HungrySingleton {
  2. private final static HungrySingleton hungrySingleton;
  3. static {
  4. hungrySingleton=new HungrySingleton();
  5. }
  6. public static HungrySingleton getInstance(){
  7. return hungrySingleton;
  8. }
  9. }

序列化与反序列化破坏单例模式:

何为对象序列化&反序列化?
序列化和反序列化是java中进行数据存储和数据传输的一种方式.
序列化: 将对象转换为字节的过程。
反序列化: 将字节转换为对象的过程。
说明:在当前软件行业中有时也会将对象转换为字符串的过程理解为序列化,例如将对象转换为json格式的字符串。
序列化的应用场景?
序列化和反序列化通常应用在:
网络通讯(C/S): 以字节方式在网络中传输数据
数据存储(例如文件,缓存)
说明:项目一般用于存储数据的对象通常会实现序列化接口.便于基于java中的序列化机制对对象进行序列化操作.
接下来破坏单利模式:
根据饿汉式单利模式举例子

  1. public class Test {
  2. public static void main(String[] args) throws IOException, ClassNotFoundException {
  3. HungrySingleton instance = HungrySingleton.getInstance();
  4. ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton_file"));
  5. oos.writeObject(instance);
  6. System.out.println("序列化 ok");
  7. //oos.close();
  8. ObjectInputStream in=
  9. new ObjectInputStream(new FileInputStream("singleton_file"));
  10. HungrySingleton hungrySingleton=(HungrySingleton)in.readObject();
  11. System.out.println(instance);
  12. System.out.println(hungrySingleton);
  13. System.out.println(instance==hungrySingleton);
  14. }
  15. }

运行会出现如下错误,这是因为什么那?因为这个类没有实现序列化接口,implements Serializable

  1. Exception in thread "main" java.io.NotSerializableException: com.qjc.pattern.creational.singleton.HungrySingleton
  2. at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
  3. at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
  4. at com.qjc.pattern.creational.singleton.Test.main(Test.java:31)

然后实现序列化接口继续运行,如下结果,显然不是同一个对象他又创建了一个对象,返回的false
在这里插入图片描述
那我加上一个私有的方法 猜猜会返回什么 readResolve()

  1. public class HungrySingleton implements Serializable {
  2. private final static HungrySingleton hungrySingleton;
  3. static {
  4. hungrySingleton=new HungrySingleton();
  5. }
  6. public static HungrySingleton getInstance(){
  7. return hungrySingleton;
  8. }
  9. private Object readResolve(){
  10. return hungrySingleton;
  11. }
  12. }

运行结果如下:
在这里插入图片描述
jdk源码我就不粘贴了 粘贴最主要的,我用文字描述,他的执行过程,执行过程通过反射inreadObject()读取序列化的反序列化对象可以定义一个readResolve方法,对其他方可见的对象,就是返回原对象,还是返回的原来的,并没有使用读取的序列化对象,而是用的原来的对象,进行破坏
在这里插入图片描述
反射攻击如何解决:

  1. public class HungrySingleton implements Serializable {
  2. private final static HungrySingleton hungrySingleton;
  3. static {
  4. hungrySingleton=new HungrySingleton();
  5. }
  6. public static HungrySingleton getInstance(){
  7. return hungrySingleton;
  8. }
  9. private Object readResolve(){
  10. return hungrySingleton;
  11. }
  12. private HungrySingleton() {
  13. }
  14. }

接下来运行结果:
改变权限 ,如果不改变权限,就会出现,因为私有构造器,我将权限进行改变,不会出现异常

  1. Class objectClass=HungrySingleton.class;
  2. Constructor constructor=objectClass.getDeclaredConstructor();
  3. //constructor.setAccessible(true);//改变权限
  4. HungrySingleton instance = HungrySingleton.getInstance();
  5. HungrySingleton hungrySingleton=(HungrySingleton)constructor.newInstance();
  6. System.out.println(instance);
  7. System.out.println(hungrySingleton);
  8. System.out.println(instance==hungrySingleton);
  9. Exception in thread "main" java.lang.IllegalAccessException: Class com.qjc.pattern.creational.singleton.Test can not access a member of class com.qjc.pattern.creational.singleton.HungrySingleton with modifiers "private"
  10. at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
  11. at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
  12. at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
  13. at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
  14. at com.qjc.pattern.creational.singleton.Test.main(Test.java:48)
  15. Process finished with exit code 1

我改变了权限:我就可以访问了,这样的话反射攻击,如何解决?
在这里插入图片描述
解决方案:
因为静态代码块,在一开始加载就会被初始化,所以其实不会为null的,如果在通过反射去创建对象,是禁止的直接抛出异常

  1. public class HungrySingleton implements Serializable {
  2. private final static HungrySingleton hungrySingleton;
  3. private HungrySingleton() {
  4. //私有构造器中加入如下代码
  5. if(hungrySingleton!=null){
  6. throw new RuntimeException("禁止反射");
  7. }
  8. }
  9. static {
  10. hungrySingleton=new HungrySingleton();
  11. }
  12. public static HungrySingleton getInstance(){
  13. return hungrySingleton;
  14. }
  15. private Object readResolve(){
  16. return hungrySingleton;
  17. }
  18. }

出现异常

  1. Exception in thread "main" java.lang.reflect.InvocationTargetException
  2. at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  3. at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
  4. at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
  5. at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
  6. at com.qjc.pattern.creational.singleton.Test.main(Test.java:48)
  7. Caused by: java.lang.RuntimeException: 禁止反射
  8. at com.qjc.pattern.creational.singleton.HungrySingleton.<init>(HungrySingleton.java:11)
  9. ... 5 more

懒汉式的反射攻击如何解决?
饿汉式的,类加载并没有初始化,然后通过反射,去调用私有构造方法,又创建了对象,正常再去调用,会是不同的对象 ,那该如何做那? 就算在私有构造方法里面加入开关,但是反射也可以修改私有属性,显然是不安全的,反射就会拿两个对象了
看如下这个逻辑是很正常的

  1. public class LazySingleton {
  2. private static LazySingleton lazySingleton=null;
  3. private static boolean flag=true;
  4. private LazySingleton(){
  5. if(flag){
  6. flag=false;
  7. }else{
  8. throw new RuntimeException("禁止反射");
  9. }
  10. }
  11. public synchronized static LazySingleton getInstance(){
  12. if(!Optional.ofNullable(lazySingleton).isPresent()){
  13. lazySingleton=new LazySingleton();
  14. }
  15. return lazySingleton;
  16. }
  17. public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  18. Class c=LazySingleton.class;
  19. Constructor constructor=c.getDeclaredConstructor();
  20. constructor.setAccessible(true);
  21. LazySingleton o1=(LazySingleton)constructor.newInstance();
  22. LazySingleton o2 = LazySingleton.getInstance();
  23. System.out.println(o1);
  24. System.out.println(o2);
  25. System.out.println(o1==o2);
  26. }
  27. }

执行结果:达到了预期,但是我在改一下 反射

  1. Exception in thread "main" java.lang.RuntimeException: 禁止反射
  2. at com.qjc.pattern.creational.singleton.LazySingleton.<init>(LazySingleton.java:17)
  3. at com.qjc.pattern.creational.singleton.LazySingleton.getInstance(LazySingleton.java:22)
  4. at com.qjc.pattern.creational.singleton.LazySingleton.main(LazySingleton.java:32)

我加入如下代码:

  1. public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
  2. Class c=LazySingleton.class;
  3. Constructor constructor=c.getDeclaredConstructor();
  4. constructor.setAccessible(true);
  5. LazySingleton o1=(LazySingleton)constructor.newInstance();
  6. Field flag = c.getDeclaredField("flag");
  7. flag.set(o1, true);
  8. LazySingleton o2 = LazySingleton.getInstance();
  9. System.out.println(o1);
  10. System.out.println(o2);
  11. System.out.println(o1==o2);
  12. }

第一次获取将flag设置为false了,但是我通过反射将他设置为true ,运行结果两个不同的对象,无论逻辑多复杂,在构造方法中,但是成员变量始终会被修改。单例懒汉模式,怎么做都防不住反射的

  1. com.qjc.pattern.creational.singleton.LazySingleton@74a14482
  2. com.qjc.pattern.creational.singleton.LazySingleton@1540e19d
  3. false

单例的模式还有一种就是单例模式不会被反射,也不会被破坏,继续往下看
使用枚举类改为单例模式:
枚举类型实现单例模式,是最佳的选择,多线程情况下也不会出现问题,反射的情况下,与序列化破坏
序列化破坏:

  1. public enum EnumInstance {
  2. INSTANCE;
  3. private Object data;
  4. public Object getData() {
  5. return data;
  6. }
  7. public void setData(Object data) {
  8. this.data = data;
  9. }
  10. public static EnumInstance getInstance(){
  11. return INSTANCE;
  12. }
  13. }

测试代码:

  1. EnumInstance enumInstance=EnumInstance.getInstance();
  2. enumInstance.setData(new Object());
  3. ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton_file"));
  4. oos.writeObject(enumInstance);
  5. System.out.println("序列化 ok");
  6. //oos.close();
  7. ObjectInputStream in=
  8. new ObjectInputStream(new FileInputStream("singleton_file"));
  9. EnumInstance instance=(EnumInstance)in.readObject();
  10. System.out.println(enumInstance.getData());
  11. System.out.println(instance.getData());
  12. System.out.println(enumInstance.getData()==instance.getData());

执行结果:
他们显然是相等的,枚举类对于序列化是不受影响的

  1. 序列化 ok
  2. java.lang.Object@5fd0d5ae
  3. java.lang.Object@5fd0d5ae
  4. true

反射攻击
答案是不能反射攻击的,因为什么那,这里我进行了反编译

  1. public enum EnumInstance {
  2. INSTANCE{
  3. protected void printTest(){
  4. System.out.println("测试拉");
  5. }
  6. };
  7. protected abstract void printTest();
  8. private Object data;
  9. public Object getData() {
  10. return data;
  11. }
  12. public void setData(Object data) {
  13. this.data = data;
  14. }
  15. public static EnumInstance getInstance(){
  16. return INSTANCE;
  17. }
  18. }

反编译:

  1. public abstract class EnumInstance extends Enum
  2. {
  3. public static EnumInstance[] values()
  4. {
  5. return (EnumInstance[])$VALUES.clone();
  6. // 0 0:getstatic #2 <Field EnumInstance[] $VALUES>
  7. // 1 3:invokevirtual #3 <Method Object _5B_Lcom.qjc.pattern.creational.singleton.EnumInstance_3B_.clone()>
  8. // 2 6:checkcast #4 <Class EnumInstance[]>
  9. // 3 9:areturn
  10. }
  11. public static EnumInstance valueOf(String name)
  12. {
  13. return (EnumInstance)Enum.valueOf(com/qjc/pattern/creational/singleton/EnumInstance, name);
  14. // 0 0:ldc1 #5 <Class EnumInstance>
  15. // 1 2:aload_0
  16. // 2 3:invokestatic #6 <Method Enum Enum.valueOf(Class, String)>
  17. // 3 6:checkcast #5 <Class EnumInstance>
  18. // 4 9:areturn
  19. }
  20. private EnumInstance(String s, int i)
  21. {
  22. super(s, i);
  23. // 0 0:aload_0
  24. // 1 1:aload_1
  25. // 2 2:iload_2
  26. // 3 3:invokespecial #7 <Method void Enum(String, int)>
  27. // 4 6:return
  28. }
  29. protected abstract void printTest();
  30. public Object getData()
  31. {
  32. return data;
  33. // 0 0:aload_0
  34. // 1 1:getfield #8 <Field Object data>
  35. // 2 4:areturn
  36. }
  37. public void setData(Object data)
  38. {
  39. this.data = data;
  40. // 0 0:aload_0
  41. // 1 1:aload_1
  42. // 2 2:putfield #8 <Field Object data>
  43. // 3 5:return
  44. }
  45. public static EnumInstance getInstance()
  46. {
  47. return INSTANCE;
  48. // 0 0:getstatic #9 <Field EnumInstance INSTANCE>
  49. // 1 3:areturn
  50. }
  51. public static final EnumInstance INSTANCE;
  52. private Object data;
  53. private static final EnumInstance $VALUES[];
  54. static
  55. {
  56. INSTANCE = new EnumInstance("INSTANCE", 0) {
  57. protected void printTest()
  58. {
  59. System.out.println("\u6D4B\u8BD5\u62C9");
  60. // 0 0:getstatic #2 <Field PrintStream System.out>
  61. // 1 3:ldc1 #3 <String "\u6D4B\u8BD5\u62C9">
  62. // 2 5:invokevirtual #4 <Method void PrintStream.println(String)>
  63. // 3 8:return
  64. }
  65. {
  66. // 0 0:aload_0
  67. // 1 1:aload_1
  68. // 2 2:iload_2
  69. // 3 3:aconst_null
  70. // 4 4:invokespecial #1 <Method void EnumInstance(String, int, EnumInstance$1)>
  71. // 5 7:return
  72. }
  73. }
  74. ;
  75. // 0 0:new #10 <Class EnumInstance$1>
  76. // 1 3:dup
  77. // 2 4:ldc1 #11 <String "INSTANCE">
  78. // 3 6:iconst_0
  79. // 4 7:invokespecial #12 <Method void EnumInstance$1(String, int)>
  80. // 5 10:putstatic #9 <Field EnumInstance INSTANCE>
  81. $VALUES = (new EnumInstance[] {
  82. INSTANCE
  83. });
  84. // 6 13:iconst_1
  85. // 7 14:anewarray EnumInstance[]
  86. // 8 17:dup
  87. // 9 18:iconst_0
  88. // 10 19:getstatic #9 <Field EnumInstance INSTANCE>
  89. // 11 22:aastore
  90. // 12 23:putstatic #2 <Field EnumInstance[] $VALUES>
  91. //* 13 26:return
  92. }
  93. }

看到了私有构造方法,和静态成员变量,通过静态代码块进行赋值操作,初始化就会被赋值,通过反射,也创建不了实例,也不支持反射newInstatice枚举类

  1. public static final EnumInstance INSTANCE;
  2. static{
  3. }

讲到这里,大家对单例模式,有了新的认识,接下来我还有干活,单例模式的,单例模式是最简单的,也是最复杂的,以后面试的时候可不能在这里丢分
ThreadLocal单例模式,这个单例模式是带有引号的,保证不了单例模式是唯一的,但是线程我能保证唯一单个线程获取到的,对象保证是唯一的,时间换空间加上volatile,空间换时间就使用ThreadLocal

  1. public class ThreadLocalInstance {
  2. private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
  3. = ThreadLocal.withInitial(() -> new ThreadLocalInstance());
  4. private ThreadLocalInstance(){
  5. }
  6. public static ThreadLocalInstance getInstance(){
  7. return threadLocalInstanceThreadLocal.get();
  8. }
  9. }
  10. System.out.println(ThreadLocalInstance.getInstance());
  11. System.out.println(ThreadLocalInstance.getInstance());
  12. System.out.println(ThreadLocalInstance.getInstance());
  13. System.out.println(ThreadLocalInstance.getInstance());
  14. System.out.println(ThreadLocalInstance.getInstance());
  15. new Thread(()->System.out.println(ThreadLocalInstance.getInstance())).start();

执行结果:由此看出,带引号的单例,不同的场景用不同的单例模式,所以单线程下我能保证唯一性,多线程下就保证不了,因为不同的线程下获取的对象不同了

  1. com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
  2. com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
  3. com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
  4. com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
  5. com.qjc.pattern.creational.singleton.ThreadLocalInstance@682a0b20
  6. com.qjc.pattern.creational.singleton.ThreadLocalInstance@5deb05a4

发表评论

表情:
评论列表 (有 0 条评论,343人围观)

还没有评论,来说两句吧...

相关阅读

    相关 23设计模式(1)-模式

    定义:         单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实