设计模式系列(创建型模式)之三单例模式

单例模式

单例模式指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
单例模式有 3 个特点:
单例类只有一个实例对象;
该单例对象必须由单例类自行创建;
单例类对外提供一个访问该单例的全局访问点;

优点:
内存中只有一个实例,减少了内存开销;
可以避免对资源的多重占用;
设置全局访问点,严格控制访问。

缺点:
没有接口,拓展困难。

单例模式的主要角色有:
单例类:包含一个实例且能自行创建这个实例的类。
访问类:使用单例的类。

1.懒汉模式

懒汉模式下的单例写法是最简单的,但它是线程不安全的:

public class LazyMode {
    //定义一个静态实例
    private static LazyMode lazyMode=null;

    public LazyMode() {
    }
    public static LazyMode getInstance(){
        if(lazyMode==null){
            lazyMode=new LazyMode();
        }
        return lazyMode;
    }
}

2.同步锁单例模式

加上一个同步锁解决下线程安全问题:

//同步锁单例模式,加同步锁,解决线程安全
public class LazyMode2 {
    //定义一个静态实例
    private static LazyMode2 lazyMode=null;

    public LazyMode2() {
    }
    public static LazyMode2 getInstance(){
        synchronized (LazyMode2.class){//加上同步锁
            if(lazyMode==null){
                lazyMode=new LazyMode2();
            }
        }
        return lazyMode;
    }
}

但是这个同步锁锁的是整个类,比较消耗资源,并且即使运行内存中已经存在LazyMode2,调用其getInstance还是会上锁。

3.双重同步锁单例模式

我们再进行优化一下,看下面的例子,在同步锁上再加上一层判断,变成双重同步锁,当LazyMode3 实例创建好后,后续再调用其getInstance方法不会上锁。
要注意的是虽然弄了双重同步锁,但它可能还是线程不安全的。虽然不会出现多次初始化LazyMode3 实例的情况,但是由于指令重排的原因,某些线程可能会获取到空对象,后续对该对象的操作将触发空指针异常。要修复这个问题,只需要阻止指令重排即可,所以可以给LazyMode3 属性加上volatile关键字,来确保线程安全。
volatile关键字修饰的成员变量具有两大特性:保证了该成员变量在不同线程之间的可见性;禁止对该成员变量进行重排序,也就保证了其有序性。但是volatile修饰的成员变量并不具有原子性,在并发下对它的修改是线程不安全的

//懒汉模式,加上双重同步锁
public class LazyMode3 {
    //定义一个静态实例
    private volatile static LazyMode3 lazyMode=null;//加上volatile关键字阻止指令重排,通过volatile修饰的成员变量  	会添加内存屏障来阻止JVM进行指令重排优化。

    public LazyMode3() {
    }
    public static LazyMode3 getInstance(){
        if(lazyMode==null) {//再加上一层判断,防止多次加锁
            synchronized (LazyMode3.class) {//加上同步锁
                if (lazyMode == null) {
                    lazyMode = new LazyMode3();
                }
            }
        }
        return lazyMode;
    }
}

4.静态内部类单例模式

JVM在类的初始化阶段会加Class对象初始化同步锁,同步多个线程对该类的初始化操作;
静态内部类InnerClass的静态成员变量lazyStaticMode在方法区中只会有一个实例。
在Java规范中,当以下这些情况首次发生时,class类将会立刻被初始化:
1.class类型实例被创建;
2.class类中声明的静态方法被调用;
3.class类中的静态成员变量被赋值;
4.class类中的静态成员被使用(非常量);

//静态内部类单例模式
public class LazyStaticMode {
    public LazyStaticMode() {
    }
    public static class InnerClass{//在内部类中定义一个静态实例
        private static LazyStaticMode lazyStaticMode=new LazyStaticMode();
    }
    public static LazyStaticMode getInstance(){
        return InnerClass.lazyStaticMode;//通过内部类去获取实例
    }
}

5.饿汉单例模式

该模式是在类加载的时候就初始化:

//饿汉单例模式
public class HungaryMode {
    private static HungaryMode hungaryMode=new HungaryMode();//直接创建实例
    public HungaryMode(){

    }
    public static HungaryMode getInstance(){
        return hungaryMode;
    }
}

这种模式在类加载的时候就完成了初始化,所以并不存在线程安全性问题;但由于不是懒加载,饿汉模式不管需不需要用到实例都要去创建实例,如果创建了不使用,则会造成内存浪费。
而且这个模式容易被序列化和反射破坏,下面做个序列化和反射的破坏例子

6.序列化破坏单例模式

1.让前面的饿汉单例模式,,实现序列化接口

//序列化破坏单例模式,实现序列化接口
public class HungaryMode2 implements Serializable{
    private static final long serialVersionUID = -9086351862017233122L;
    private static HungaryMode2 hungaryMode=new HungaryMode2();
    public HungaryMode2(){

    }
    public static HungaryMode2 getInstance(){
        return hungaryMode;
    }
}

2.测试输出

public class Test {
    public static  void main(String[]args) throws IOException, ClassNotFoundException {
        HungaryMode2 hungaryMode2=HungaryMode2.getInstance();
        //对象输出
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
        outputStream.writeObject(hungaryMode2);
        //对象输入
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file"));
        HungaryMode2 hungaryMode3 = (HungaryMode2) inputStream.readObject();
        System.out.println(hungaryMode2);
        System.out.println(hungaryMode3);
        System.out.println(hungaryMode2==hungaryMode3);
    }
}
//com.wfg.design.mode5.HungaryMode2@4a574795
//com.wfg.design.mode5.HungaryMode2@f6f4d33
//false

从输出可以看到,即使它是单例模式,也成功创建出了两个不一样的实例,单例遭到了破坏。
要让反序列化后的对象和序列化前的对象是同一个对象的话,可以在HungaryMode2 里加上readResolve方法:
这种方式反序列化过程内部还是会重新创建HungaryMode2 实例,但是因为HungaryMode2 类定义了readResolve方法(方法内部返回了hungaryMode对象引用),反序列化过程中会判断目标类是否定义了readResolve该方法,是的话则会通过反射调用该方法,所以最终获取到的还是HungaryMode2

//序列化破坏单例模式,实现序列化接口
public class HungaryMode2 implements Serializable{
    private static final long serialVersionUID = -9086351862017233122L;
    private static HungaryMode2 hungaryMode=new HungaryMode2();
    public HungaryMode2(){

    }
    public static HungaryMode2 getInstance(){
        return hungaryMode;
    }
    public Object readResolve(){//添加上readResolve方法
        return hungaryMode;
    }
}

测试输出

public class Test {
    public static  void main(String[]args) throws IOException, ClassNotFoundException {
        HungaryMode2 hungaryMode2=HungaryMode2.getInstance();
        //对象输出
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
        outputStream.writeObject(hungaryMode2);
        //对象输入
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file"));
        HungaryMode2 hungaryMode3 = (HungaryMode2) inputStream.readObject();
        System.out.println(hungaryMode2);
        System.out.println(hungaryMode3);
        System.out.println(hungaryMode2==hungaryMode3);
    }
}
//com.wfg.design.mode5.HungaryMode2@4a574795
//com.wfg.design.mode5.HungaryMode2@4a574795
//true

7.反射破坏单例模式

public static  void main(String[]args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        HungaryMode2 hungaryMode2=HungaryMode2.getInstance();
        //通过反射创建了HungaryMode2实例
        Class<HungaryMode2> cs=HungaryMode2.class;
        //获取HungaryMode2里的构造
        Constructor<HungaryMode2> ct=cs.getConstructor();
        //打开构造权限
        ct.setAccessible(true);

        HungaryMode2 hungaryMode3=ct.newInstance();
        System.out.println(hungaryMode2);
        System.out.println(hungaryMode3);
        System.out.println(hungaryMode2==hungaryMode3);
    }
    //com.wfg.design.mode5.HungaryMode2@27716f4
	//com.wfg.design.mode5.HungaryMode2@8efb846
	//false

从上面输出可以看到,可以通过反射破坏了私有构造器权限,成功创建了新的实例。
对于这种情况,饿汉模式下的例子可以在构造器中添加判断逻辑来防御(懒汉模式的就没有办法了)

//序列化破坏单例模式,实现序列化接口
public class HungaryMode2 implements Serializable{
    private static final long serialVersionUID = -9086351862017233122L;
    private static HungaryMode2 hungaryMode=new HungaryMode2();
    public HungaryMode2(){
        if(hungaryMode==null){//通过在这里加上判断来阻止反射破坏
            throw new RuntimeException("不允许通过这个构造器来生成实例");
        }
    }
    public static HungaryMode2 getInstance(){
        return hungaryMode;
    }
    public Object readResolve(){
        return hungaryMode;
    }
}

再次模拟反射,发现抛出我们的自己定义的错误,阻止反射破坏实例

Exception in thread "main" java.lang.ExceptionInInitializerError
	at com.wfg.design.mode5.Test.main(Test.java:23)
Caused by: java.lang.RuntimeException: 不允许通过这个构造器来生成实例
	at com.wfg.design.mode5.HungaryMode2.<init>(HungaryMode2.java:11)
	at com.wfg.design.mode5.HungaryMode2.<clinit>(HungaryMode2.java:8)
	... 1 more

8.枚举单例模式

枚举单例模式是推荐的单例模式,它不仅可以防御序列化破坏,也可以防御反射破坏
1.定义一个枚举类,里面设置想要的实例对象

public enum EnumMode {
    ENUMMODE;
    private Object obj;

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }

    public static EnumMode getInstance(){
        return ENUMMODE;
    }
}

2.测试是否是单例的

 public static  void main(String[]args)  {
        EnumMode enumMode=EnumMode.getInstance();
        enumMode.setObj(new Object());
        EnumMode enumMode2=EnumMode.getInstance();
        System.out.println(enumMode.getObj());
        System.out.println(enumMode2.getObj());
        System.out.println(enumMode==enumMode2);
    }
    //java.lang.Object@27716f4
	//java.lang.Object@27716f4
	//true

从输出上看到,是单例的,还是同一个对象
3.测试序列化破坏

//序列化破坏
    public static  void main(String[]args) throws IOException, ClassNotFoundException {
        EnumMode enumMode=EnumMode.getInstance();
        enumMode.setObj(new Object());
        //对象输出
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
        outputStream.writeObject(enumMode);
        //对象输入
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file"));
        EnumMode enumMode2 = (EnumMode) inputStream.readObject();
        System.out.println(enumMode.getObj());
        System.out.println(enumMode2.getObj());
        System.out.println(enumMode==enumMode2);
    }
    //java.lang.Object@4a574795
	//java.lang.Object@4a574795
	//true

从输出上看到,创建的实例对象,也没有被序列化破坏,还是同一个

//反射破坏
    public static  void main(String[]args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumMode enumMode=EnumMode.getInstance();
        enumMode.setObj(new Object());
        //通过反射创建了HungaryMode2实例
        Class<EnumMode> cs=EnumMode.class;
        // 枚举类只包含一个(String,int)类型构造器
        Constructor<EnumMode> ct=cs.getDeclaredConstructor(String.class,int.class);
        //打开构造权限
        ct.setAccessible(true);

        EnumMode enumMode2=ct.newInstance("aa",1);
        System.out.println(enumMode);
        System.out.println(enumMode2);
        System.out.println(enumMode==enumMode2);
    }

抛出异常

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.wfg.design.mode5.Test2.main(Test2.java:42)

由于Java禁止通过反射创建枚举对象,抛出了错误。正是因为枚举类型拥有这些先天的优势,所以用它创建单例也是不错的选择,
下一篇讲讲建造者模式

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值