设计模式--单例模式的实现方式
设计模式–单例模式的实现方式
90%程序员第一个听过的设计模式一定是单例模式,我觉得是因为其实现简单,理解起来容易,才如此流行。正因为它的知名度和流行度,单例模式的实现方式有很多,而且一直在更新。我们今天就讨论下目前主流生成单例模式的方式。
我们听过最多的单例模式实现方式是饿汉式,懒汉式,但其实还有静态内部类等其他方式。
1.饿汉式
public class Singleton { private static Singleton singleton = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return singleton; } }
这种方式很多人都会写,但这样写单例模式有利有弊,优点是不用担心并发,静态变量只会初始化一次,缺点是如果引用了这个类的其他静态变量,就会创建该类的对象,但你可能永远都不会使用这个对象,造成内存的浪费,所以不推荐使用这种方式
2.懒汉式
public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); } return singleton; } }
懒汉式就不会有这个问题
刚开始学习 java 的时候,可能会这样写,但随着学习的深入,你会发现这样写有并发问题,试想线程1 刚判断完 singleton 为 null,准备去新建实例,线程2 也走到了判断 singleton 这一步,此时 singleton 还是空,线程2也新建了一个实例,这就不能保证单例了,因此懒汉式我们都是用下面这种方式
3.懒汉式(同步锁)
public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
这种方式通过加同步锁保证对象的唯一性,这里我们判断了两次 singleton,原因是,如果线程1,2都在外层判断了 singleton 为空,线程1先拿到了同步锁,创建了一个新对象,线程2等待,线程1完成后,线程2也拿到了同步锁,如果内层不再判断 singleton 是否为空,线程2就会再次创建一个对象,这就无法保证对象的唯一性了。由于加了同步锁,这种方式在高并发下也能保证对象的唯一性。但这种写法还是会有一个问题,jvm 创建一个对象执行三步
1)堆内存开辟内存空间
2)在堆内存中实例化SingleTon里面的各个参数
3)对象指向堆内存空间
由于 jvm会优化指令,可能在执行第二步之前先执行了第三步,另一个线程走到这一步看到对象是非空的,直接拿来用,就会发生异常。后来 JDK1.5后,官方也发现了这个问题就发明了 volatile 关键字,下面这样写就没问题了
public class DCLSafeSingleton { //添加 volatile 关键字,防止指令重排 private static volatile DCLSafeSingleton singleton; private DCLSafeSingleton() {} public static DCLSafeSingleton getInstance() { if (singleton == null) { synchronized (DCLSafeSingleton.class) { if (singleton == null) { singleton = new DCLSafeSingleton(); } } } return singleton; } }
4.静态内部类
public class InnerClassSingleton{ public static InnerClassSingleton getInstance() { return Instance.singleton; } private InnerClassSingleton(){} private static class Instance { static InnerClassSingleton singleton = new InnerClassSingleton(); } }
这种是算是比较好的单例模式写法了,静态内部类只加载一次,保证了线程安全,只有调用 getInstance 方法才会加载内部类,创建实例,不会造成内存的浪费。但这种方式有个缺点,由于是内部类形式,所以创建对象时无法传参。虽然这种方法看上去比较安全了,但可能存在反射攻击或反序列化攻击。如下面代码
反射攻击
public static void main(String[] args) throws Exception { InnerClassSingleton singleton = InnerClassSingleton.getInstance(); Constructor<InnerClassSingleton> constructor = InnerClassSingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); InnerClassSingleton newSingleton = constructor.newInstance(); System.out.println(singleton == newSingleton); }
运行结果是 false
反序列化攻击
先引入依赖
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8.1</version> </dependency>
代码
public class Singleton implements Serializable { private static class Instance{ private static Singleton instance = new Singleton(); } private Singleton() {} public static Singleton getInstance() { return Instance.instance; } public static void main(String[] args) { Singleton instance = Singleton.getInstance(); byte[] serialize = SerializationUtils.serialize(instance); Singleton newInstance = SerializationUtils.deserialize(serialize); System.out.println(instance == newInstance); } }
运行结果也是 false
5.枚举法
《Effective java 》这本书写这是实现单例模式最好的方法。以上优点它都有,还不用担心有反射攻击或反序列化攻击,如下图
调用方法:
public enum EnumSingleton { INSTANCE; public void getSomething() { System.out.println("getSomething"); } //调用方式 public static void main(String[] args) { EnumSingleton.INSTANCE.getSomething(); } }
以上就是单例模式主要的创建方式啦,园友们可以根据具体业务场景进行选择合适的实现方式。