带你看到一个不一样的单例模式

带你看到不一样的单例模式

单例模式

单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。在设计模式中,单例是属于创建型模式,在我们使用的servletContext、servletContextConfig等都是单例的体现形式。单例究竟有多少写法,没有一个明确的说法,更多的是一种思想在单个类上的体现。在这里我大致分类四类: 1、饿汉式 2、懒汉式 3、注册式 4、序列化。下面依次阐述

饿汉式

饿汉式单例在类加载的时候就立即初始化,并且创建单例对象。它是线程安全的,在线程没有出现以前就实例化了。写法:

//最常见的写法
public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() { }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}
// 静态内部类写法
public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungryStaticSingleton ;

    static {
        hungryStaticSingleton = new HungryStaticSingleton();
    }
    private HungryStaticSingleton(){}

    public static HungryStaticSingleton getInstance() {
        return hungryStaticSingleton;
    }
}

这两种写法,并没本质上的区别,也很好理解,在类被加载的时候,我们就创建了对象。
优点:线程安全,执行效率较高
缺点:如果存在大量对象的时候,对系统的内存占用大,特别是一堆不需要使用的对象,造成严重的内存浪费。
所以我们进行优化,就开始引入懒汉式单例。

懒汉式

为了解决饿汉式单例造成的资源浪费问题,我们采用懒汉式单例,只有在需要的时候,才会被创建。

public class LazySimpleSingleton {
    private static LazySimpleSingleton instance;
    private LazySimpleSingleton() {}

    public static LazySimpleSingleton 	getInstance() {
        if(null == instance){
        //当有多个线程执行到这里的时候,对象会被创建多个
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}
//对线程安全和效率问题做了个优化,采用双重锁校验
public class LazyDoubleSingleton {
    private static volatile LazyDoubleSingleton lazyDoubleSingleton;
    private LazyDoubleSingleton(){}

    public static LazyDoubleSingleton getInstance() {
        // 检查是否需要阻塞
        if (null == lazyDoubleSingleton) {
            synchronized (LazyDoubleSingleton.class) {
                //检测是否需要创建对象
                if (null == lazyDoubleSingleton) {
                    return new LazyDoubleSingleton();
                }
                // 指令重排序问题
            }
        }
        return lazyDoubleSingleton;
    }
}

/*
classPath:LazyStaticInnerClassSingleton.class
          LazyStaticInnerClassSingleton$LazyHolder.class
   优点:写法简单,很好的利用了Java本身的语法特点,性能高,避免了内存浪费
   缺点:可以被反射破坏
 */
public class LazyStaticInnerClassSingleton {
    private LazyStaticInnerClassSingleton(){
        if (LazyHolder.instance != null) {
            throw new RuntimeException("不允许非法访问");
        }
    }

    private static LazyStaticInnerClassSingleton getInstance() {
        return LazyHolder.instance;
    }

    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton instance = new LazyStaticInnerClassSingleton();
    }
}

在懒汉式中,第一种写法除了线程不安全,容易出现伪单例,测试时我们获取到了一个值,其实是创建了两次对象,而对对象进行了一个覆盖赋值。最后一种写法算是利用java本身的语法特点,进行了一个投机取巧的方式,我们加载的时候不会去加载LazyHolder内部的静态变量,在调用的时候当做对象来调用,注释上写的LazyStaticInnerClassSingleton$LazyHolder.class

注册式

注册式单例又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例,注册式单例分为两种,一种是枚举式,一种是容器式单例。

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;
    }
}

public class ContainerSingleton {
    private ContainerSingleton(){}

    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();

    public static Object getBean(String className)  {
        Object instance = null;

        //1、检测是否需要阻塞
        if (!ioc.containsKey(className)) {
            synchronized (ContainerSingleton.class) {
                // 2、检测是否需要创建对象
                if (!ioc.containsKey(className)) {
                    try {
                        instance = Class.forName(className).newInstance();
                        ioc.put(className, instance);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                return instance;
                }
            }
        }
        return ioc.get(className);
    }
}

spring采用的就是容器式单例,当然spring的逻辑比上面这个例子要复杂的多,后期我们会慢慢讲。
枚举为什么是单例的,我们反编译会发现以下代码:

static{
	INSTANCE = new EnumSingleton("INSTANCE"0);
	$VALUES = (new EnumSingleton[]{INSTANCE});
}

所以枚举式单例也是饿汉式单例,如果我们尝试用反射去破坏会怎么样?

	public static void main(String[] args){
		try{
			Class clazz=EnumSingleton.class;
			Constructor c = clazz.getDeclaredConstructor();
			c.newInstance();
		}catch(Exception e){
		e.printStachTrace();
		}
	}

结果报java.lang.NoSuchMethodException异常,意识就是没有找到无参的构造方法,我们在看Enum源码

protected Enum(String name ,int ordinal){
  this.name = name;
  this.ordinal = ordinal;
  }

如果用暴力破解,能破坏枚举单例吗?这个大家可以去思考,并且测试一下。
枚举式单例模式是JDK最官方、稳定和权威的处理,因此也是在《Effective Java》中推荐的方式

序列化

对象写好,我们把对象序列化写入磁盘,下次再反序列转为内存对象,反序列化会重新分配内存地址,就等于重新创建了新的对象。

public class SeriableSingleton implements Serializable {
    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance() {
        return INSTANCE;
    }

    private Object readResolve() {
        return INSTANCE;
    }
}

我们加一个readResoulve方法就可以解决了,因为在ObjectInputStream中的readObject()方法中重写了readObject()方法,底层的invokeReadResolve()方法中,反射调用了readResolveMethod方法,找到无参的readResolve()保存下来。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值