【设计模式系列6】单例模式的8种写法及如何防止单例被破坏

深入分析java单例模式

  • 设计模式系列总览
  • 什么是单例模式
  • 单例模式的常见写法
    • 一、饿汉式单例
      • 优点
      • 缺点
      • 示例
    • 二、懒汉式单例
      • 示例1(普通写法)
      • 示例2(synchronized写法)
      • 示例3(DCL写法)
      • 示例4(内部类写法)
    • 三、注册式单例
      • 示例1(容器式)
      • 示例2(枚举式)
    • 四、ThreadLocal式单例
      • 示例
  • 总结

设计模式系列总览

设计模式 飞机票
三大工厂模式 登机入口
策略模式 登机入口
委派模式 登机入口
模板方法模式 登机入口
观察者模式 登机入口
单例模式 登机入口
原型模式 登机入口
代理模式 登机入口
装饰者模式 登机入口
适配器模式 登机入口
建造者模式 登机入口
责任链模式 登机入口
享元模式 登机入口
组合模式 登机入口
门面模式 登机入口
桥接模式 登机入口
中介者模式 登机入口
迭代器模式 登机入口
状态模式 登机入口
解释器模式 登机入口
备忘录模式 登机入口
命令模式 登机入口
访问者模式 登机入口
软件设计7大原则和设计模式总结 登机入口

什么是单例模式

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式属于创建型模式

单例模式的常见写法

一、饿汉式单例

顾名思义饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就被实例化了,不可能存在访问安全问题

优点

没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好

缺点

类加载的时候就初始化,不管用与不用都占着空间,如果项目中有大量单例对象,则可能会浪费大量内存空间

示例

package com.zwx.design.pattern.singleton.hungry;

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

    private HungrySingleton() {
     
    }
    
    public static HungrySingleton getInstance(){
     
        return hungrySigleton;
    }
}

或者也可以利用静态代码块的方式实现饿汉式单例

package com.zwx.design.pattern.singleton.hungry;

public class HungryStaticSingleton {
     
    private static final HungryStaticSingleton hungrySigleton;
    static {
     
        hungrySigleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
     
    }

    public static HungryStaticSingleton getInstance(){
     
        return hungrySigleton;
    }
}

这两种写法都非常的简单,也非常好理解,饿汉式适用在单例对象较少的情况

二、懒汉式单例

懒汉式单例的特点是:被外部类调用的时候内部类才会加载

示例1(普通写法)

package com.zwx.design.pattern.singleton.lazy;

public class LazySingleton {
     
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
     
    }

    public static LazySingleton getInstance(){
     
        if(null == lazySingleton){
     
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

上面的写法是最简单的一种懒汉式单例写法,但是存在线程安全问题,多线程情况下会有一定几率返回多个单例对象,这明显违背了单例对象原则,那么如何优化上面的代码呢?答案就是加上synchronized关键字

示例2(synchronized写法)

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.hungry.HungrySingleton;

public class LazySingleton {
     
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
     
    }

    public synchronized static LazySingleton getInstance(){
     
        if(null == lazySingleton){
     
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

示例2的写法仅仅是在getInstance()方法上面加了synchronized关键字,其他地方没有任何变化。用 synchronized 加锁,在线程数量比较多情况下,如果CPU分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既兼顾线程安全又提升程序性能呢?答案是肯定的。接下来就在介绍一种双重检查锁(double-checked locking)单例写法

示例3(DCL写法)

package com.zwx.design.pattern.singleton.lazy;

public class LazyDoubleCheckSingleton {
     
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {
     
    }

    public static LazyDoubleCheckSingleton getInstance(){
     
        if(null == lazySingleton){
     //1
            synchronized (LazyDoubleCheckSingleton.class){
     //2
                if(null == lazySingleton){
     //3
                    lazySingleton = new LazyDoubleCheckSingleton();//4
                }
            }
        }
        return lazySingleton;//5
    }
}

这里的写法将同步放在了方法里面的第一个非空判断之后,这样可以确保对象不为空的时候不会被阻塞,但是第二个非空判断的意义是什么呢?我们假设线程A首先获得锁,进入了第3行,还没有释放锁的时候,线程B又进来了,这时候因为线程还没有执行对象初始化,所以判空成立,会进入第2行等待获得锁,这时候当线程A释放锁之后,线程B会进入到第3行,这时候因为第二个判空判断对象不为空了,所以就会直接返回,如果没有第2个判空,这时候就会产生新的对象了,所以需要两次判空!

大家可能注意到这里的变量定义上加了volatile关键字,为什么呢?这是因为DCL在可能会存在失效的情况:
第4行代码:lazySingleton = new LazyDoubleCheckSingleton();
大致存在以下三步:
(1)、分配内存给对象
(2)、初始化对象
(3)、将初始化好的对象和内存地址建立关联(赋值)
而这3步由于CPU指令重排序,不能保证一定按顺序执行,假如线程A正在执行new的操作,第1步和第3步都执行完了,但是第2步还没执行完,这时候线程B进入到方法中的第1行代码,判空不成立,所以直接返回了对象,而这时候对象并没有初始化完全,所以就会报错了,解决这个问题的办法就是使用volatile关键字,禁止指令重排序(jdk1.5之后),保证按顺序执行上面的三个步骤。想要详细了解volatile关键字是如何解决重排序问题的,可以点击这里

示例4(内部类写法)

package com.zwx.design.pattern.singleton.lazy;

public class LazyInnerClassSingleton {
     

    private LazyInnerClassSingleton(){
     
    }
    public static final LazyInnerClassSingleton getInstance(){
     
        return LazyHolder.LAZY;
    }

    private static class LazyHolder{
     
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

上面的写法巧妙的利用了内部类的特性,LazyHolder里面的逻辑需要等到外面方法调用时才执行。
这种写法看起来很完美,没有加锁,也保证了懒加载,但是这种单例模式也有问题,那就是可以被反射或者序列化破坏单例,下面我们写一个反射破坏单例的例子

package com.zwx.design.pattern.singleton.lazy;

import java.lang.reflect.Constructor;

public class LazyInnerClassSingletonTest {
     

    public static void main(String[] args) throws Exception {
     
        Class<?> clazz = LazyInnerClassSingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Object o1 = constructor.newInstance();
        Object o2 = LazyInnerClassSingleton.getInstance();
        System.out.println(o1 == o2);//false
    }
}

上面这个结果输出的结果为false,说明产生了2个对象,当然,要防止反射破坏单例很简单,我们可以把上面例子中的构造方法加一个判断就可以了:

 private LazyInnerClassSingleton(){
     
        //防止反射攻击
       if(null != LazyHolder.LAZY){
     
           throw new RuntimeException("不允许构造多个实例");
       }
    }

这样虽然防止了反射破坏单例,但是依然可以被序列化破坏单例,下面就让我们验证一下序列化是如何破坏单例的!
首先对上面的类实现序列化接口

public class LazyInnerClassSingleton implements Serializable

接下来开始对单例对象类进行序列化和反序列化测试:

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.seriable.SeriableSingleton;

import java.io.*;
import java.lang.reflect.Constructor;

public class LazyInnerClassSingletonTest {
     

    public static void main(String[] args) throws Exception {
     
        LazyInnerClassSingleton s1 = null;
        LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();

        FileOutputStream fos = null;

        try {
     
            fos = new FileOutputStream("LazyInnerClassSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (LazyInnerClassSingleton)ois.readObject();
            ois.close();
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);//false
        }catch (Exception e){
     
            e.printStackTrace();
        }
    }
}

这时候输出结果为false,说明产生了2个对象,那么我们应该如何防止序列化破坏单例呢?我们可以对LazyInnerClassSingleton类加上readResolve方法就可以防止序列化破坏单例

package com.zwx.design.pattern.singleton.lazy;

import java.io.Serializable;

public class LazyInnerClassSingleton implements Serializable {
     

    private LazyInnerClassSingleton(){
     
        //防止反射攻击
       if(null != LazyHolder.LAZY){
     
           throw new RuntimeException("不允许构造多个实例");
       }
    }
    //防止序列化破坏单例
    private Object readResolve(){
     
        return LazyHolder.LAZY;
    }
    public static final LazyInnerClassSingleton getInstance(){
     
        return LazyHolder.LAZY;
    }

    private static class LazyHolder{
     
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

这是因为JDK源码中会检验一个类中是否存在一个readResolve()方法,如果存在,则会放弃通过序列化产生的对象,而返回原本的对象,也就是说,在校验是否存在readResolve()方法前产生了一个对象,只不过这个对象会在发现类中存在readResolve()方法后丢掉,然后返回原本的单例对象,保证了单例的唯一性,这种写法虽然保证了单例唯一,但是过程中类也是会被实例化两次,假如创建对象的频率增大,就意味着内存分配的开销也随之增大,那么有没有办法从根本上解决问题呢?那么下面就让继续介绍一下注册式单例

三、注册式单例

注册式单例就是将每一个实例都保存到某一个地方,然后使用唯一的标识获取实例

示例1(容器式)

package com.zwx.design.pattern.singleton.register;

public class ContainerSingleton {
     
    private ContainerSingleton(){
     
    }

    private static Map<String,Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(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;
            }
            return ioc.get(className);
        }
    }
}

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的,spring中的单例就是属于此种写法

示例2(枚举式)

package com.zwx.design.pattern.singleton.register;

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;
    }
   
}

枚举式单例是《Effective java》一书中推荐的写法,这种写法避免了上面的内部类写法中存在的问题(虽然结果唯一,但是过程产生了多个实例对象),是一种效率较高的写法

四、ThreadLocal式单例

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

示例

package com.zwx.design.pattern.singleton.threadlocal;

public class ThreadLocalSingleton {
     
    private ThreadLocalSingleton() {
     

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

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

测试

package com.zwx.design.pattern.singleton.threadlocal;

import com.zwx.design.pattern.singleton.ExectorThread;
import com.zwx.design.pattern.singleton.ExectorThread3;

public class ThreadLocalSingletonTest {
     

    public static void main(String[] args) {
     
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + ":" + singleton);
            }
        });
        t1.start();
    }
}

【设计模式系列6】单例模式的8种写法及如何防止单例被破坏_第1张图片
反复测试可以发现同一个线程获得的对象是唯一的,不同对象则不唯一

总结

单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用,单例模式的写法很多,大家可以根据自己的业务需求选择合适自己的单例方式

你可能感兴趣的:(设计模式,java,设计模式)