java基础 - 设计模式 - 单例模式

单例模式是一种创建型模式,用于创建对象,它的特点在于该类全局只有一个对象实例,并且对外提供一个公共入口来访问这个实例。

常见实现方式

1、饿汉式的单例模式实现【可用,但不推荐】

public class MySingleton {

	//私有的静态成员变量,在类加载时直接创建对象
    private static final MySingleton instance = new MySingleton();

	//私有的构造方法,防止外部调用来创建对象
    private MySingleton() {
    }

	//对外访问单例对象的入口
    public static MySingleton getInstance() {
        return instance;
    }
}

这种方式属于饿汉式,也即是说它比较着急,在类加载过程中就实例化了单例对象,而不是实际用到的时候再创建(比如调用getInstance方法时)。
这种方式虽然是线程安全的,但问题在于不是真正需要的时候才创建对象,没有起到懒加载的作用,造成了不必要的内存占用。

2、懒汉式的单例模式实现【线程不安全,不可用】

public class MySingleton {

    private static MySingleton instance;

    private MySingleton() {
    }

    public static MySingleton getInstance() {
        if (instance == null) {
            instance = new MySingleton();
        }
        return instance;
    }
}

现在对象的创建转移到了 getInstance 方法里,实现了懒加载,但是很明显,这种写法是线程不安全的,对象创建完成之前,其他线程读到了instance == null,也会去创建,就不符合单例原则了。

3、双检锁 + volatile实现【推荐】

public class MySingleton {

    private static volatile MySingleton instance;

    private MySingleton() {
    }

    public static MySingleton getInstance() {
        if (instance == null) {
            //synchronized锁之所以加在这里而不是最外层,是为了减少不必要的同步,提高效率
            synchronized (MySingleton.class) {
                //这里第二次判空,正是因为加锁范围较小,第一次判空并没有同步
                if (instance == null) {
                    /*
                    注意这条语句并非原子操作,内部有可能发生指令重排,导致对象未初始化时先将地址暴露给了instance引用,
                    其他线程判断到instance不为null,则会直接使用该对象,出现线程安全问题。
                    所以instance属性必须使用volatile关键字修饰,从而禁止相关的指令重排。
                    */
                    instance = new MySingleton();
                }
            }
        }
        return instance;
    }
}

这种实现方式是懒汉式的改进实现,懒加载的同时保证了线程安全。

4、静态内部类实现【推荐】

public class MySingleton {

    private MySingleton() {
    }

    private static class InstanceHolder {
        static final MySingleton instance = new MySingleton();
    }

    public static MySingleton getInstance() {
        return InstanceHolder.instance;
    }
}

静态内部类 InstanceHolder 只有在被使用到的时候才会进行类加载(也就是调用getInstance方法时),然后创建instance对象,从而实现了懒加载。同时单例对象跟随类的初始化而创建,保证了线程安全。

5、枚举实现【推荐】

public enum MySingleton {
    INSTANCE;

    public static void main(String[] args) {
        MySingleton mySingleton = MySingleton.INSTANCE;
    }
}

枚举是最简单且安全的实现方式,它不仅避免了线程安全问题,而且还能天然地防御反射和反序列化的方式对单例模式的破坏。

反射对单例模式的破坏和防御

反射机制可以直接调用类的构造器来创建对象,而不需要调用 MySingleton 的 getInstance方法,这样每次反射调用 newInstance 方法都会产生的新的对象,从而破坏掉单例模式。
上述的几种实现方式中,只有枚举方式的实现,能够天然地防御反射破坏,因为JDK代码中通过反射机制,调用构造器的newInstance方法时,会检查类型是否为枚举类型,如果是则会抛异常。也就是说java中不允许通过反射的方式构造枚举实例。

而对于其他方式,我们必须从构造方法入手,通过编码的方式防止反射破坏。
1、饿汉式和静态内部类的方式
这两种方式的共同点在于,都是在类的初始化过程中,实例化了单例对象;而通过反射api,比如 newInstance 方法创建对象时,就会先触发类加载和初始化,所以对于这两种方式,我们在构造函数中添加实例是否为空的判断,即可防止单例模式被反射破坏。

饿汉式:

public class MySingleton {

    private static final MySingleton instance = new MySingleton();

    //此处加个判断
    private MySingleton() {
        if (instance != null) {
            throw new RuntimeException("单例对象已经存在");
        }
    }

    public static MySingleton getInstance() {
        return instance;
    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("org.example.thread.MySingleton");
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        MySingleton mySingleton1 = (MySingleton) constructor.newInstance();
        MySingleton mySingleton2 = (MySingleton) constructor.newInstance();
        System.out.println(mySingleton1);
        System.out.println(mySingleton2);
    }
}

运行结果:
在这里插入图片描述
静态内部类方式:

public class MySingleton {

	//此处加个判断
    private MySingleton() {
        if(InstanceHolder.instance != null){
            throw new RuntimeException("单例对象已经存在");
        }
    }

    private static class InstanceHolder {
        static final MySingleton instance = new MySingleton();
    }

    public static MySingleton getInstance() {
        return InstanceHolder.instance;
    }
}

通过反射调用构造方法时,会先触发内部类InstanceHolder的类加载和初始化,从而创建单例对象,所以这里的条件恒为true,然后就会报错。而正常的使用getInstance方法,仅会触发一次构造函数的调用,不会抛异常。

2、双检锁方式
对于该方式,修改构造函数并不能完全防止单例被反射所破坏,比如:

    private MySingleton() {
        if(instance != null){
            throw new RuntimeException("单例对象已经存在");
        }
    }

这里 instance != null 的前提是调用过 getInstance 方法生成了单例对象,但是 如果在调用getInstance之前,直接连续地使用反射来创建对象,则对象仍然不是单例的。

反序列化对单例模式的破坏和防御

以静态内部类的实现方式为例,反序列化对单例模式的破坏如下:

public class MySingleton implements Serializable {

    private MySingleton() {
    }

    private static class InstanceHolder {
        static final MySingleton instance = new MySingleton();
    }

    public static MySingleton getInstance() {
        return InstanceHolder.instance;
    }

    public static void main(String[] args) throws Exception {
        MySingleton mySingleton1 = MySingleton.getInstance();

        //序列化
        FileOutputStream fileOutputStream = new FileOutputStream("MySingleton.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(mySingleton1);
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("MySingleton.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        MySingleton mySingleton2 = (MySingleton) objectInputStream.readObject();

        System.out.println(mySingleton1);
        System.out.println(mySingleton2);
    }
}

执行mai方法,结果如下:
在这里插入图片描述
反序列化的实质还是通过反射调用了构造器的newInstance方法,创建出了新的对象。不过反序列化的破坏也是可以防御的,在readObject方法中,会检查并调用类中定义的 readResolve方法,如果该方法返回了对象,那么反序列化就会使用readResolve返回的对象作为结果。所以我们需要做的就是在类中添加一个readResolve方法。如下:

    private Object readResolve() {
        return InstanceHolder.instance;
    }

再次运行会发现,反序列化得到的对象为原对象。

对于枚举方式实现的单例,不用担心反序列化带来的破坏,因为在java的反序列化中,对枚举类型的反序列化,不会创建新的实例。

clone()方法对单例模式的破坏和防御

实现了Cloneable接口的单例类,调用其对象的clone()方法,也会创造出新的对象,破坏单例模式(枚举除外,它不允许被clone)。
可以通过重写clone()方法直接返回已有的实例对象来解决。

总结:
饿汉式、双检锁式、静态内部类、枚举类型都是可用的单例模式实现方案,可以根据需求不同进行选择,当然最推荐的还是枚举方式的实现,不仅实现简单,而且天然免疫反射、反序列化、clone()等任何形式的破坏,非常安全。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值