单例模式应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。在Spring框架中ApplicationContext就是单例模式的应用。

饿汉式单例

饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。线程安全。
优点:没有加任何的锁、执行效率比较高。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。

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

    private HungrySingleton(){
   }

    public static final HungrySingleton getINSTANCE(){
   
        return INSTANCE;
    }
}

还有另一种写法,利用静态代码块的方式:

public class HungrySingleton {
   
    private static final HungrySingleton INSTANCE;
    static {
   
        INSTANCE = new HungrySingleton();
    }
    private HungrySingleton(){
   }
    public static final HungrySingleton getINSTANCE(){
   
        return INSTANCE;
    }
}

以上写法都非常简单,饿汉式单例适用于在单例对象较少的情况下。

懒汉式

懒汉式单例的特点是:被外部类调用的时候内才会创建实例。

  1. 非线程安全
//懒汉式单例 //在外部需要使用的时候才进行实例化 
public class LazySimpleSingleton {
    
    private LazySimpleSingleton(){
   } 
    //静态块,公共内存区域 
    private static LazySimpleSingleton lazy = null; 
    public static LazySimpleSingleton getInstance(){
    
        if(lazy == null){
    
            lazy = new LazySimpleSingleton(); 
        }
        return lazy; 
} 

以上写法在多线程调用的情况下会出现线程安全问题,有一定几率创建多个实例。
所以我们可以对以上代码进行修改,就是上锁:

  1. 方法加锁
public class LazySimpleSingleton {
    
    private LazySimpleSingleton(){
   } 
    //静态块,公共内存区域 
    private static LazySimpleSingleton lazy = null; 
    public  synchronized static LazySimpleSingleton getInstance(){
    
        if(lazy == null){
    
            lazy = new LazySimpleSingleton(); 
        }
        return lazy; 
} 

此种加锁方式在多线程情况下会导致线程阻塞,程序运行性能大幅下降。所以我们可以进一步升级:

  1. 双重校验锁
public class LazyDoubleCheckSingleton {
   
    private volatile static LazyDoubleCheckSingleton lazy;
    private LazyDoubleCheckSingleton() {
   
    }
    public static LazyDoubleCheckSingleton getInstance() {
   
        if (lazy == null) {
   
            synchronized (LazyDoubleCheckSingleton.class) {
   
                if (lazy == null) {
   
                    lazy = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazy;
    }
} 

此种方法采用了双重校验锁的方式,在实例创建完成后,多线程调用的情况下也不会阻塞线程。但是此种方式在多线程都来初始化实例的时候就会形成锁竞争,也会对性能造成影响。
我们可以采用一种更为优雅的写法:

  1. 静态内部类
public class StaticInnerClassHurrySingleton {
   
    private StaticInnerClassHurrySingleton(){
   }
    public static final StaticInnerClassHurrySingleton getINSTANCE(){
   
        return Holder.SINGLETON;
    }
    private static class Holder{
   
        private static final StaticInnerClassHurrySingleton SINGLETON = new StaticInnerClassHurrySingleton();
    }
}

静态内部类的写法既没有饿汉式的内存浪费问题,也没有加锁带来的性能开销。内部类一定是在方法调用之前初始化的,巧妙的避免了线程安全问题。

单例模式的破坏

反射破坏单例

大家有没有发现,上面介绍的单例模式的构造方法除了加上private以外,没有做任何处理。如果我们使用反射来调用其构造方法,然后,再调用getInstance()方法,就可以创建两个不同的实例。现在来看一段测试代码,以StaticInnerClassHurrySingleton为例:

public class StaticInnerClassHurrySingletonTest {
   
    public static void main(String[] args) {
   
        try {
   
            //很无聊的情况下,进行破坏
            Class<?> clazz = StaticInnerClassHurrySingleton.class;
            //通过反射拿到私有的构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //强制访问
            c.setAccessible(true);
            //暴力初始化
            Object o1 = c.newInstance();
            //调用了两次构造方法,相当于 new了两次
            Object o2 = c.newInstance();
            System.out.println(o1 == o2);
        } catch (Exception e) {
   
            e.printStackTrace();
        }
    }
}

运行结果:false
显然创建了两个不同的实例,我们可以在构造方法中添加现在来,一旦出现多次创建,则直接抛出异常:

public class StaticInnerClassHurrySingleton {
   
    private StaticInnerClassHurrySingleton(){
   
        //添加实例检查
        if(Holder.SINGLETON!=null){
   
            //抛出异常
            throw new RuntimeException("实例已被创建");
        }
    }
    public static final StaticInnerClassHurrySingleton getINSTANCE(){
   
        return Holder.SINGLETON;
    }
    private static class Holder{
   
        private static final StaticInnerClassHurrySingleton SINGLETON = new                   StaticInnerClassHurrySingleton();
    }
}

继续上面的测试,结果:

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.hiwei.pattern.singleton.hunger.LazyInnerClassSingletonTest.main(LazyInnerClassSingletonTest.java:19)
Caused by: java.lang.RuntimeException: 实例已被创建
	at com.hiwei.pattern.singleton.hunger.StaticInnerClassHurrySingleton.<init>(StaticInnerClassHurrySingleton.java:10)
	... 5 more

反射破坏单例漏洞就被修复掉了。

序列化破坏单例

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:

public class SeriableSingleton implements Serializable {
   
    /* 序列化: 把内存中的状态通过转换成字节码的形式 从而转换一个 IO 流,写入到其他地方(可以是磁盘、网络 IO) 内存中状态给永久保存下来了 反序列化: 将已经持久化的字节码内容,转换为 IO 流 //通过 IO 流的读取,进而将读取的内容转换为 Java 对象 在转换过程中会重新创建对象new */
    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton() {
   
    }
    public static SeriableSingleton getInstance() {
   
        return INSTANCE;
    }
}

测试代码:

public class SeriableSingletonTest {
   
    public static void main(String[] args) {
   
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();
        FileOutputStream fos = null;
        try {
   
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton) ois.readObject();
            ois.close();
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        } catch (Exception e) {
   
            e.printStackTrace();
        }
    }
}

测试结果:

com.hiwei.pattern.singleton.hunger.SeriableSingleton@6acbcfc0
com.hiwei.pattern.singleton.hunger.SeriableSingleton@60e53b93
false

结果表明单例模式已经被序列化破坏掉了。怎么避免呢?
增加readResolve()方法即可。来看优化代码:

/** * @author: hiwei * @create: 2018-06-25 22:24 */
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; }
}

仍由上面测试方法测试,测试结果:

com.hiwei.pattern.singleton.hunger.SeriableSingleton@60e53b93
com.hiwei.pattern.singleton.hunger.SeriableSingleton@60e53b93
true

显然问题被解决了。
为什么加一个方法就解决了这个问题?我们可以看下JDK的源码,就是ObjectInputStream类的readObject()方法,通过这个方法我们可以看到以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。这部分代码因为篇幅较多,就不在此解析了。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大。下面我们可以采用另一种方式来实现单例:

注册式单例

注册式单例有两种写法:一种为容器缓存,一种为枚举登记。

  1. 枚举式单例
    枚举式单例的写法,来看代码,创建EnumSingleton类:
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; 
    } 
}

使用反编译工具来反编译该类,可以得到以下代码:

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

枚举式单例在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例的实现。

  1. 容器式单例
public class ContainerSingleton {
   
    private ContainerSingleton(){
   }
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    public static Object getInstance(String className){
   
        synchronized (ioc) {
   
            if (!ioc.containsKey(className)) {
   
                Object obj = null;
                try {
   
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
   
                    e.printStackTrace();
                }
                return obj;
            } else {
   
                return ioc.get(className);
            }
        }
    }
}

spring的IOC容器使用的就是这种单例模式。

ThreadLocal线程单例

线程单例实现 ThreadLocal。ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,线程安全。

public class ThreadLocalSingleton {
   
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            new ThreadLocal<ThreadLocalSingleton>(){
   
                @Override
                protected ThreadLocalSingleton initialValue() {
   
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton(){
   }

    public static ThreadLocalSingleton getInstance(){
   
        return threadLocalInstance.get();
    }
}

总结

自此单例模式都实现了。单例模式保证了类内存里面只有一个内存实例,减少了内存开销,避免了对内存资源的多重占用