戏说设计模式——单例模式

 前言

        想一想,如果公司里面有一个大牛,一个人就能把所有的事情都干了,那该多好!作为一个不喜欢卷来卷去的中国时代打工人,我喜欢这样的同事。作为吹着**春风成长起来的企业家,同样喜欢这样的员工(可以少聘用几个打工人了!!!可以裁掉几个打工人了!!!),可以省下不少成本!!!

         如你所愿,单例模式就是这么一个时代楷模,虽然不能包揽所有工作,但是分内之事,一人足矣!接下来,来看一个简单的实例吧。

实例一:

package com.elean.design.singleton;

/**
 * @auther Elean
 * @date 17/7/2024 下午4:23
 * @description
 */
public class SuperMan {
    public static SuperMan tom = new SuperMan();
    
    //拒绝招聘
    private SuperMan() {
    }

    //有事找Tom
    public static final SuperMan getInstance() {
        return tom;
    }

    public void serveTea() {
        System.out.println("端茶,我来");
    }

    public void pourWater() {
        System.out.println("倒水,我来");
    }
}

        在这个案例里面,我们可以看到单例的几个最基本特点:

                1)构造函数私有(拒绝招聘新人,Tom is super man)

                2)有一个类变量来承载单一实例

                3)给一个静态方法获取实例

        当然,如果老板突然有一天喜欢上骑马、烧柴,就需要老实肯干的Tom另外学习喂马、劈柴的技能,如果没涨工资,这就对Tom是很不公平了(也违背了开闭原则:对扩展开放,对修改关闭)。这时候,就需要老板再开放一个岗位主管所有部门的马和柴,为了进一步节约成本,老板决定,招柴买马那天再设此岗位。

实例二:

package com.elean.design.singleton;

/**
 * @auther Elean
 * @date 17/7/2024 下午4:23
 * @description
 */
public class BatMan {
    public static BatMan xiaoMing;

    private BatMan() {
    }

    public static final BatMan getInstance() {
        if (null == xiaoMing) {
            xiaoMing = new BatMan();
        }
        return xiaoMing;
    }

    public void horseFeed() {
        System.out.println("喂马,我去");
    }

    public void choppingFirewood() {
        System.out.println("劈柴,我去");
    }
}

一、单线程下的使用及存在的问题

1.1 饿汉模式和懒汉模式

        以上两个示例是单例模式中两种比较简单的写法:饿汉模式和懒汉模式

        对比两种写法来看,饿汉模式在类的初始化过程会自动调用它的实例化方法给Tom赋值,而后一直存在于内存中;懒汉模式则是在调用该类的instance()方法时才会进行实例化。从表面上看,懒汉模式对资源的占用相对更小一些。但是我们知道,用户自定义类只有在被使用,或者子类被加载的时候才会被加载,如果我们能够做到让类的加载时间跟使用实例的时间一致,那么这种消耗是可以避免的,这就要求我们熟练掌握jvm的类加载机制,小心使用饿汉模式了。

1.2 反射破坏单例

        在单线程中,一般情况下上面两个简单示例是完全可以保证内存中单例了。当然,如果我们使用反射把构造函数设置成true同样可以在内存中创建实例,这种情况,可以在构造函数中像懒汉模式一样先判断内存中是否有实例存在,如下代码。

package com.elean.design.singleton;

import java.lang.reflect.Constructor;

/**
 * @auther Elean
 * @date 18/7/2024 下午8:40
 * @description
 */
public class SingleReflect {
    public static void main(String[] args) {
        SuperMan instance = SuperMan.getInstance();
        try {
            Class<?> clazz = Class.forName("com.elean.design.singleton.SuperMan");
            Constructor<?> constructor = clazz.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            Object o = constructor.newInstance();
            System.out.println(instance);
            System.out.println(o);
            System.out.println(instance == o); //打印结果:false
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在构造函数中判空:饿汉模式和懒汉模式都可以用这种方式解决

public class SuperMan {
    ...

    private SuperMan() {
        if (null != tom) {
            throw new RuntimeException("请不要破坏单例结构。。。");
        }
    }

    ...
}

1.3 序列化破坏单例

        也有大佬提供了另外一种破坏单例的方法——序列化,以及解决办法(参考:设计模式之单例模式详解和应用-CSDN博客

package com.elean.design.singleton;

import java.io.*;

/**
 * @auther Elean
 * @date 19/7/2024 上午9:33
 * @description
 */
public class BreakSingle {
    public static void main(String[] args) {
        SuperMan instance = SuperMan.getInstance();

        try (
                ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SuperMan.txt"));
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream("SuperMan.txt"))
        ){
            oos.writeObject(instance);
            Object o = ois.readObject();

            System.out.println(instance);
            System.out.println(o);
            System.out.println(o == instance); //打印结果:false
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

解决序列化破环单例:添加readResove()方法

package com.elean.design.singleton;

import java.io.Serializable;

/**
 * @auther Elean
 * @date 17/7/2024 下午4:23
 * @description
 */
public class SuperMan implements Serializable {
    public static SuperMan tom = new SuperMan();

    private SuperMan() {
        if (null != tom) {
            throw new RuntimeException("请不要破坏单例结构。。。");
        }
    }

    public static final SuperMan getInstance() {
        return tom;
    }

    private Object readResolve() {
        return tom;
    }

    public void serveTea() {
        System.out.println("端茶,我来");
    }

    public void pourWater() {
        System.out.println("倒水,我来");
    }
}

        在我看来,如果不希望序列化去破坏单例的话,我们实在没必要让这个类实现Serializable接口。当然,如果非要说什么特殊情况,谨慎使用吧!!!

二、多线程下的单例模式

2.1 多线程破坏懒汉模式

        饿汉模式是在类加载的时候就初始化了类变量,将实例存入内存,因此在多线程中不会出现单例被破坏的情况。所以这里我们主要讨论懒汉模式,下面看下被破坏的场景:

  为了模拟,我们让静态实例方法在判空后先睡一会

package com.elean.design.singleton;

/**
 * @auther Elean
 * @date 17/7/2024 下午4:23
 * @description
 */
public class BatMan {
    ...
    public static final BatMan getInstance() {
        if (null == xiaoMing) {

            /**************模拟多线程同时进入的情况*******************/
            try {
                System.out.println(Thread.currentThread().getName() + "调用实例");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /******************************************************/

            xiaoMing = new BatMan();
        }
        return xiaoMing;
    }

    ...
}

测试代码:

package com.elean.design.singleton;

/**
 * @auther Elean
 * @date 19/7/2024 上午9:33
 * @description
 */
public class BreakSingle {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            BatMan instance1 = BatMan.getInstance();
            System.out.println(instance1);
        });
        thread.start();
        BatMan instance = BatMan.getInstance();
        System.out.println(instance);
    }
}

打印结果:

main调用实例
Thread-0调用实例
com.elean.design.singleton.BatMan@3b9a45b3
com.elean.design.singleton.BatMan@22e7a307

         从打印结果来看,主线程和线程0调用静态实例方法创建的实例的内存地址是不一样的,也就是创建了不同的实例。当不同的线程同时调用静态实例方法的时候,可能出现线程a还未创建实例时,线程b调用了为空的判断,这时,两个线程都会运行创建实例的代码。因此,在实际项目应用中,通常都会用以下两种方式来防止单例被破坏:

2.2 双重校验锁

package com.elean.design.singleton;

/**
 * @auther Elean
 * @date 17/7/2024 下午4:23
 * @description
 */
public class BatMan {
    ...
    public static final BatMan getInstance() {
        if (null == xiaoMing) {

            /**************模拟多线程同时进入的情况*******************/
            try {
                System.out.println(Thread.currentThread().getName() + "调用实例");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /******************************************************/

            synchronized (BatMan.class) {
                if (null == xiaoMing) {
                    xiaoMing = new BatMan();
                }
            }
        }
        return xiaoMing;
    }

    ...
}

        双重校验锁保证了在多线程的情况下只有一个线程能够创建实例。但同一时间进来的线程,因为拿不到锁会被阻塞掉,会影响被阻塞线程的响应速度。

2.3 静态内部类

package com.elean.design.singleton;

/**
 * @auther Elean
 * @date 17/7/2024 下午4:23
 * @description
 */
public class BatMan {
    
    private BatMan() {
    }

    public static final BatMan getInstance() {
        return InstanceInner.xiaoMing;
    }
       
    //InstanceInner和外部类会被编译成两个文件,InstanceInner在使用时才会被加载
    private static class InstanceInner{
        public static final BatMan xiaoMing = new BatMan();
    }

    public void horseFeed() {
        System.out.println("喂马,我去");
    }

    public void choppingFirewood() {
        System.out.println("劈柴,我去");
    }
}

打印结果:

main调用实例
Thread-0调用实例
com.elean.design.singleton.BatMan@3b9a45b3
com.elean.design.singleton.BatMan@3b9a45b3

三、单例模式的其他实现方式

3.1 枚举类实现单例

package com.elean.design.singleton;

/**
 * @auther Elean
 * @date 20/7/2024 下午6:53
 * @description
 */
public enum EnumInstance {
    XIAO_MING;

    public void horseFeed() {
        System.out.println("喂马,我去");
    }

    public void choppingFirewood() {
        System.out.println("劈柴,我去");
    }
}

 测试代码:

package com.elean.design.singleton;

/**
 * @auther Elean
 * @date 21/7/2024 上午10:48
 * @description
 */
public class EnumTest {
    public static void main(String[] args) {
        EnumInstance.XIAO_MING.horseFeed();
        EnumInstance.XIAO_MING.choppingFirewood();
    }
}

打印结果:

喂马,我去
劈柴,我去

         枚举类之所以能够实现单例,我们对代码进行编译(javac)之后反编译(javap -c && javap -p)的代码如下。在代码中我们看到实例是被static和final修饰的,而其被实例化是在静态代码块中调用了<init>的方法实现的。这和我们的饿汉式单例模式实现如出一辙。

javap -p EnumInstance.class

public final class com.elean.design.singleton.EnumInstance extends java.lang.Enum<com.elean.design.singleton.EnumInstance> {
  public static final com.elean.design.singleton.EnumInstance XIAO_MING;                                                    
  private static final com.elean.design.singleton.EnumInstance[] $VALUES;                                                   
  public static com.elean.design.singleton.EnumInstance[] values();                                                         
  public static com.elean.design.singleton.EnumInstance valueOf(java.lang.String);
  private com.elean.design.singleton.EnumInstance();
  public void horseFeed();
  public void choppingFirewood();
  static {};
}

 javap -c EnumInstance.class

 static {};
    Code:
       0: new           #4                  // class com/elean/design/singleton/EnumInstance
       3: dup
       4: ldc           #11                 // String XIAO_MING
       6: iconst_0
       7: invokespecial #12                 // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #13                 // Field XIAO_MING:Lcom/elean/design/singleton/EnumInstance;
      13: iconst_1
      14: anewarray     #4                  // class com/elean/design/singleton/EnumInstance
      17: dup
      18: iconst_0
      19: getstatic     #13                 // Field XIAO_MING:Lcom/elean/design/singleton/EnumInstance;
      22: aastore
      23: putstatic     #1                  // Field $VALUES:[Lcom/elean/design/singleton/EnumInstance;
      26: return

        枚举实现饿汉单例模式从jdk中对类实例化的代码实现来看,先天拒绝了通过反射破坏单例。通过 getDeclaredConstructor() 方法,获取枚举类的所有构造函数,枚举类只有一个(String,int) 的构造函数;当通过该构造函数的 newInstance() 方法获取实例时,jdk拒绝了我们的实例化请求,原因无它,jdk在constructor方法中判断了修饰符是否ENUM,如果是ENUM就会抛出异常,代码如下:

@CallerSensitive
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
{
    ...
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ...
}

        同理,在反序列化的代码中同样有对枚举类型的判断,而readEnum(...)方法中用Enum的valueOf方法通过类类型和枚举名称在常量中获取枚举实例,也就是说,获取的是同一个实例。

/**
 * readObject0()
 */
...
case TC_ENUM:
    if (type == String.class) {
       throw new ClassCastException("Cannot cast an enum to java.lang.String");
    }
    return checkResolve(readEnum(unshared));
...
private Enum<?> readEnum(boolean unshared) throws IOException {
    if (cl != null) {
       try {
           @SuppressWarnings("unchecked")
           Enum<?> en = Enum.valueOf((Class)cl, name);
           result = en;
        }
        ...
    }
    ...
}
Map<String, T> enumConstantDirectory() {
   if (enumConstantDirectory == null) {
       T[] universe = getEnumConstantsShared();
       if (universe == null)
           hrow new IllegalArgumentException(getName() + " is not an enum type");
       Map<String, T> m = new HashMap<>(2 * universe.length);
       for (T constant : universe)
          m.put(((Enum<?>)constant).name(), constant);
       enumConstantDirectory = m;
   }
   return enumConstantDirectory;
}

3.2 注册式单例

 public class ContainerSingleton {
     private ContainerSingleton(){}
     private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
     public static Object getInstance(String className){
         Object instance = null;
         if(!ioc.containsKey(className)){
             try {
                 instance = Class.forName(className).newInstance();
                 ioc.put(className, instance);
             }catch (Exception e){
                 e.printStackTrace();
             }
             return instance;
         }else{
             return ioc.get(className);
         }
     }
 }

         单例模式在java开源框架中的应用还是比较多的,比如Spring IOC的容器,线程池连接,数据库连接池等的实现,让我们一起掌握好单例的应用,在java开发中大展拳脚吧!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值