设计模式 单例设计模式(Singleton Pattern)详细解读

一、什么是单例设计模式?

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

二、适用场景

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等

三、设计思想演进

问题: 该如何实现单例设计模式呢?怎样才能保证只创建一个实例化对象?如何提供访问途径?
1、首先我们创建一个类,类名为Config01,让该类在加载的时候就持有一个自己的实例INSTANCE。

public class Config01 {
    // static 是为了可以直接类名.INSTANCE,否则还要先再new 一个Config01才能访问该变量,违背了单例的意愿
    public static Config01 INSTANCE = new Config01()
}

2、要实现单例,下面我们就要考虑的是如何防止外界随便访问创建的INSTANCE实例和new出一个新的实例。

public class Config01 {
    // 在类的内部创建一个类的实例对象,该静态修饰的对象随着类加载只创建一次实例
    private static final Config01 INSTANCE = new Config01();
    //私有化构造器,使得在类的外部不能够调用此构造器,随意创建实例对象
    private Config01() {}
}

3、此时获取INSTANCE路径已经完全封闭了起来,如果想获取就要我们自己提供一个公共的getInstance方法,如下

public class Config01 {
    private static final Config01 INSTANCE = new Config01();

    private Config01() {}

    public static Config01 getInstance() {
        return INSTANCE;
    }
}

4、到这里我们已经实现了一个饿汉式的单例设计模式,下面进行测试看是否获得的实例是同一个

public class test{
    public static void main(String[] args) {
        Config01 m1 = Config01.getInstance();
        Config01 m2 = Config01.getInstance();
        System.out.println(m1 == m2);
    }
}

结果为true

四、饿汉式

饿汉式的特点就是在类一加载的时候就会创建一个自己的实例对象,占用了内存空间。

结果

public class Config01 {
    private static final Config01 INSTANCE = new Config01();

    private Config01() {}

    public static Config01 getInstance() {
        return INSTANCE;
    }
}

也可放在静态代码块中初始化

/**
 * 放在静态代码块种初始化,与01相同
 */
public class Config02 {
    private static final Config02 INSTANCE;

    static {
        INSTANCE = new Config02();
    }

    private Config02() {}

    public static Config02 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        Config02 m1 = Config02.getInstance();
        Config02 m2 = Config02.getInstance();
        System.out.println(m1 == m2);
    }
}

五、懒汉式-懒加载(Lazy Load)

懒汉式也就采用了懒加载的思想,这里的懒加载指的是在使用实例对象的时候才会去创建实例对象。这就避免了资源的浪费和内存的占用问题。

初步改造

public class Config03 {
    private volatile static Config03 INSTANCE;

    private Config03() {}

    public static Config03 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Config03();
        }
        return INSTANCE;
    }
}

线程安全问题和解决思路

所谓单例设计模式中的线程不安全,就是在多线程调用中存在可以创建多个该实例对象的现象!

1、我们对上面代码进行测试看看再多线程下是否能创建同一实例对象

public static void main(String[] args) {
    for(int i=0; i<100; i++) {
        new Thread(()->System.out.println(Config03.getInstance())).start();
    }
}

打印部分结果如下

com.wanniwa.dp.singleton.Config03@43d83830
com.wanniwa.dp.singleton.Config03@43d83830
com.wanniwa.dp.singleton.Config03@43d83830
com.wanniwa.dp.singleton.Config03@43d83830

居然结果一致,难道是线程安全的,理论出了问题?

2、猜想是否是因为cpu执行过快,导致getInstance()方法瞬间被执行完毕,我们修改Config03中getInstance()中代码

public static Config03 getInstance() {
    if (INSTANCE == null) {
        //sleep 1 millis为了放大没优化前懒汉式单例的线程安全问题
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        INSTANCE = new Config03();
    }
    return INSTANCE;
}

再次执行测试代码结果如下

com.wanniwa.dp.singleton.Config03@3d797c69
com.wanniwa.dp.singleton.Config03@4920b564
com.wanniwa.dp.singleton.Config03@33ca79ec
com.wanniwa.dp.singleton.Config03@4c48e6a8

3、操作方法加同步锁,但也带来效率下降

public class Config04 {
    private static Config04 INSTANCE;

    private Config04() {
    }

    public static synchronized Config04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Config04();
        }
        return INSTANCE;
    }
}

经过测试,符合单例

4、下面就要考虑性能如何优化的问题了,首先就想到的是缩小synchronized的范围

public class Config05 {
    private static Config05 INSTANCE;

    private Config05() {
    }

    public static Config05 getInstance() {
        if (INSTANCE == null) {
            //妄图通过减小同步代码块的方式提高效率,然后不可行
            synchronized (Config05.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Config05();
            }
        }
        return INSTANCE;
    }
}

经过测试,不符合单例
分析原因:因为在执行了if (INSTANCE == null)判断后,CPU可能跑去执行其他线程创建了对象,再回来执行时后面没有判断的逻辑,程序会认为还没有创建,会继续创建一个新的实例对象。

结果

5、我们使用双重检查锁(double checked locking)就避免了上面的问题,有人会问为什么不把if (INSTANCE == null)直接放在同步代码块中,是因为如果先在外面判断的话,可以节省锁资源竞争。

public class Config06 {
    private static volatile Config06 INSTANCE;

    private Config06() {
    }

    public static Config06 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Config06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Config06();
                }
            }
        }
        return INSTANCE;
    }
}

为何要加volatile?

上述写法看似解决了问题,但是有个很大的隐患。实例化对象new Config06()的那行代码,实际上真正运行的指令可以分解成以下三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间

但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:

  1. 分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象

2和3的重排序不影响线程A的最终结果,但会导致线程B在第一次判断INSTANCE不为空(因为线程A已经给ISTANCE分配了内存空间,只是实例没有初始化),线程B接下来将访问INSTANCE引用的没有初始化的对象。此时,线程B将会访问到一个还未初始化的对象。

在这里本来想测试一下不加时的现象,但实验了很久没有出现,所以放弃了,如果有比较好的测试方案可以留言交流

所以只需要把instance声明为volatile型,就可以实现线程安全的延迟初始化。 因为被volatile关键字修饰的变量是被禁止重排序的。

六、静态内部类实现单例设计模式(推荐)

内部类在类加载的时候不会被实例化,实现懒加载的方式,同时类加载的时候是线程安全的。类的静态属性只会在第一次加载时初始化,JVM在帮助我们保证了线程的安全性。推荐使用。

结果

public class Config07 {
    private Config07() {
    }

    private static class Config07Holder {
        private final static Config07 INSTANCE = new Config07();
    }

    public static Config07 getInstance() {
        return Config07Holder.INSTANCE;
    }
}

七、枚举实现单例设计模式

单例的枚举实现在Effective Java一书中提到。因为其功能完善,使用简介,无偿地提供了序列化机制,在面对复杂的序列化或者反射攻击时任然可以绝对防止多次实例化等优点,被作者所推崇。

结果

public enum Config08 {

    INSTANCE;

    private Integer number;

    public void show() {
        System.out.println("配置数字:"+number);
    }
}

如果想获取实例可通过Mgr08.INSTANCE直接获得,下面进行测试

public static void main(String[] args) {
	for(int i=0; i<100; i++) {
		new Thread(()->{
			System.out.println(Mgr08.INSTANCE);
		}).start();
	}
}

经过测试,符合单例

八、问题:遇到反序列化、反射攻击怎么办?

我们拿饿汉式的代码Config1来进行测试

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

    private Config01() {};

    public static Config01 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception{
        Config01 c1=Config01.getInstance();
        Config01 c2=Config01.getInstance();
        System.out.println("c1内存地址:"+c1);
        System.out.println("c2内存地址:"+c1);
        System.out.println("正常情况,实例化两个实例是否相同:"+(c1==c2)+"\n");

        //反射
        Constructor<Config01> constructor=Config01.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Config01 c3=constructor.newInstance();
        System.out.println("c3内存地址:"+c3);
        System.out.println("反射,实例化两个实例是否相同:"+(c1==c3)+"\n");

        // 通过反序列化的方式构造多个对象(类需要实现Serializable接口)
        // 1. 把对象c1写入硬盘文件
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(c1);
        oos.close();
        fos.close();
        // 2. 把硬盘文件上的对象读出来
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object"));
        // 如果对象定义了readResolve()方法,readObject()会调用readResolve()方法。从而解决反序列化的漏洞
        Config01 c4 = (Config01) ois.readObject();
        // 反序列化出来的对象,和原对象,不是同一个对象。如果对象定义了readResolve()方法,可以解决此问题。
        System.out.println("c4内存地址:"+c4);
        System.out.println("反序列化,实例化两个实例是否相同:" + (c1==c4));
        ois.close();
    }
}

结果为:

c1内存地址:com.wanniwa.dp.singleton.Config01@1b6d3586
c2内存地址:com.wanniwa.dp.singleton.Config01@1b6d3586
正常情况,实例化两个实例是否相同:true

c3内存地址:com.wanniwa.dp.singleton.Config01@4554617c
反射,实例化两个实例是否相同:false

c4内存地址:com.wanniwa.dp.singleton.Config01@378bf509
反序列化,实例化两个实例是否相同:false

然后测试其他几种方式,除了枚举都出现了问题。
枚举的单例模式测试结果会报Exception in thread “main” java.lang.NoSuchMethodException。出现这个异常的原因是因为EnumSingleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,而且在反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。所以枚举是不怕发射攻击的。

public T newInstance(Object ... initargs)
	throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
	if (!override) {
		if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
			Class<?> caller = Reflection.getCallerClass();
			checkAccess(caller, clazz, null, modifiers);
		}
	}
	if ((clazz.getModifiers() & Modifier.ENUM) != 0)
		throw new IllegalArgumentException("Cannot reflectively create enum objects");
	ConstructorAccessor ca = constructorAccessor;   // read volatile
	if (ca == null) {
		ca = acquireConstructorAccessor();
	}
	@SuppressWarnings("unchecked")
		T inst = (T) ca.newInstance(initargs);
	return inst;
}

如何避免

1、使用枚举

当枚举在newInstance时,代码中做了判断会抛IllegalArgumentException,所以非常安心

2、解决反射问题

解决办法:参考枚举newInstance时抛异常的思路,在构造方法中进行判断抛出异常

  • 饿汉式
private Config01() {
	if (null != INSTANCE) {
		throw new IllegalStateException();
	}
}
  • 静态内部类
private Config07() {
	if (null != Config07Holder.INSTANCE) {
		throw new IllegalStateException();
	}
}
3、解决反序列化

如果对象定义了readResolve()方法,readObject()会调用readResolve()方法。从而解决反序列化的漏洞

private Object readResolve() {
	return INSTANCE;
}

最终测试

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

    private Config01() {
        if (null != INSTANCE) {
            throw new IllegalStateException();
        }
    }

    public static Config01 getInstance() {
        return INSTANCE;
    }

    private Object readResolve() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception{
        Config01 c1=Config01.getInstance();
        Config01 c2=Config01.getInstance();
        System.out.println("c1内存地址:"+c1);
        System.out.println("c2内存地址:"+c1);
        System.out.println("正常情况,实例化两个实例是否相同:"+(c1==c2)+"\n");

        //反射
        Constructor<Config01> constructor=Config01.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Config01 c3=constructor.newInstance();
        System.out.println("c3内存地址:"+c3);
        System.out.println("反射,实例化两个实例是否相同:"+(c1==c3)+"\n");

        // 通过反序列化的方式构造多个对象(类需要实现Serializable接口)
        // 1. 把对象c1写入硬盘文件
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(c1);
        oos.close();
        fos.close();
        // 2. 把硬盘文件上的对象读出来
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object"));
        // 如果对象定义了readResolve()方法,readObject()会调用readResolve()方法。从而解决反序列化的漏洞
        Config01 c4 = (Config01) ois.readObject();
        // 反序列化出来的对象,和原对象,不是同一个对象。如果对象定义了readResolve()方法,可以解决此问题。
        System.out.println("c4内存地址:"+c4);
        System.out.println("反序列化,实例化两个实例是否相同:" + (c1==c4));
        ois.close();
    }
}

测试结果如下,反射已经解决

c1内存地址:com.wanniwa.dp.singleton.Config01@1b6d3586
c2内存地址:com.wanniwa.dp.singleton.Config01@1b6d3586
正常情况,实例化两个实例是否相同:true

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.wanniwa.dp.singleton.Config01.main(Config01.java:41)
Caused by: java.lang.IllegalStateException
	at com.wanniwa.dp.singleton.Config01.<init>(Config01.java:19)
	... 5 more

为了不报错我们将反射测试的代码注释,继续测试结果如下,反序列化问题解决

c1内存地址:com.wanniwa.dp.singleton.Config01@1b6d3586
c2内存地址:com.wanniwa.dp.singleton.Config01@1b6d3586
正常情况,实例化两个实例是否相同:true

c4内存地址:com.wanniwa.dp.singleton.Config01@1b6d3586
反序列化,实例化两个实例是否相同:true
  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值