设计模式学习——单例模式

一、单例模式的概念

1.1 概念

单例模式是指 确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。

单例模式的特点是隐藏其所有的构造方法。

属于创建型模式。

1.2 单例模式的适用场景

确保任何情况下都绝对只有一个实例。

例如ServletContext、ServletConfig、ApplicationContext

1.3 单例模式的常见写法

1.饿汉式单例

2.懒汉式单例

二、饿汉式单例

饿汉式单例,指在类刚刚加载还没有实例化的时候就被创建实例。

2.1 实现方式一

 构造方法私有,通过static成员的特点,在类刚刚加载还没有实例化的时候就被创建实例:

/**
 * @Auther: jesses
 * @Description: 饿汉式单例实现方式一
 */
public class HungrySingleton {
    private static final HungrySingleton hungrySingleton=new HungrySingleton();

    private HungrySingleton(){}

    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

2.2 实现方式二

构造方法私有化,通过static代码块,在类刚刚加载还没有实例化的时候就被创建实例:

/**
 * @Auther: jesses
 * @Description: 饿汉式单例实现方式二
 */
public class HungrySingleton2 {
    //使用final是为了避免有人通过反射机制将它改变
    private static final HungrySingleton2 hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton2();
    }

    private HungrySingleton2(){}

    public static HungrySingleton2 getInstance(){
        return hungrySingleton;
    }
}

饿汉式单例的特点

构造方法私有化 类加载时创建实例,不会出现线程安全的问题。

浪费内存空间,因为不管是否用到,在类加载时就会创建实例。

因此需要改进,这就出现了懒汉式单例。

三、懒汉式单例

被外部调用时才会创建这个单例的实例。

3.1 实现方式一  simple实现方式:

/**
 * @Auther: jesses
 * @Description: simple实现方式
 */
public class LazySimpleSingleton {
    private static LazySimpleSingleton lazy=null;

    private LazySimpleSingleton(){}

    public static LazySimpleSingleton getInstance(){
        if (lazy == null){
            //!!存在线程安全的问题
            lazy=new LazySimpleSingleton();
        }
        return lazy;
    }
}
/**
 * @Auther: jesses
 * @Description: 线程中创建单例对象
 */
public class ExectorThread implements Runnable {
    public void run() {
        LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+" : "+instance);
    }
}
/**
 * @Auther: jesses
 * @Description: 测试类
 */
public class LazySimpleSingletonTest {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new ExectorThread());
        Thread thread2 = new Thread(new ExectorThread());
        thread1.start();
        thread2.start();
        System.out.println("Exec End .");
    }
}

 运行结果可以看到,两个线程会创建出不同的实例。

在demo中单例的实现存在线程安全的问题,若两个线程同时通过了if判断,进入if内部,会创建两个不同的对象。

因此,优化方案是在getInstance()方法上加synchronize关键字。但是synchronize性能不好,尤其修饰在static方法上,会造成整个类都被锁定。

因此,更优解是,将synchronized关键字只修饰方法内部的代码块。

/**
* 将synchronized关键字只修饰方法内部的代码块
**/
public class LazyDubboCheckSingleton {
    private static LazyDubboCheckSingleton lazy=null;

    private LazyDubboCheckSingleton(){}

    public static LazyDubboCheckSingleton getInstance(){
        if (lazy == null){
            //synchronized修饰在静态方法上可能造成整个类都被锁定,将synchronized设置在方法内部,这样线程至少可以进入方法。
            //但是两个线程都同时执行到此处时,都会得到lazy都是空,将会顺序执行到synch中的代码,又出现多次创建不同实例的情况。这种情况又需要进行改进
            synchronized (LazyDubboCheckSingleton.class){
                lazy=new LazyDubboCheckSingleton();
            }
        }
        return lazy;
    }
}

synchronized修饰在静态方法上可能造成整个类都被锁定,将synchronized设置在方法内部,这样线程至少可以进入方法。

但是两个线程都同时进入方法,又会出现得到的lazy都是空,顺序执行synchronized片段,又出现了多次创建不同实例的情况。

故而需要双重检查锁,在synchronized代码块中再加一次判断,即下面的双重检查锁的方式。

3.2 实现方式二 双重检查锁实现:

/**
 * @Auther: jesses
 */
public class LazyDubboCheckSingleton {
    private static LazyDubboCheckSingleton lazy=null;

    private LazyDubboCheckSingleton(){}

    public static LazyDubboCheckSingleton getInstance(){
        if (lazy == null){//此判断如删除则无法进入方法
            synchronized (LazyDubboCheckSingleton.class){
                //为避免出现多次创建不同实例的线程安全问题,所以需要再多加一层判断,也就是双重检查
                if (lazy == null){
                    lazy=new LazyDubboCheckSingleton();
                }
            }
        }
        return lazy;
    }
}

3.3 实现方式三 内部类实现方式:

/**
 * @Auther: jesses
 * @Description: 内部类实现懒汉式单例
 */
//没有使用到synchronized,性能最高
public class InnerClassLazySingleton {

    private InnerClassLazySingleton() {
    }

    //懒汉式单例
    //在LazyHolder里的逻辑要等到外部方法getInstance调用时才执行。
    //同时利用加载类前先加载静态内部类的特性,加载LazyHolder的空对象,但不执行其中内容。
    //因为类加载机制只会加载一次,实现了单例。
    //LazyHolder的空对象在类加载时已经生成,在调用getInstance()时,则转变成实质对象。因而,是线程安全的。
    public static final InnerClassLazySingleton getInstance() {
        return LazyHolder.LAZY;
    }

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

这种方式虽然解决了线程安全问题和性能问题。

但是它还是可能被反射攻击。

四、反射破坏单例

/**
 * @Auther: jesses
 * @Description:
 */
public class InnerClassLazySingletonTest {

    public static void main(String[] args) {
        try {
            Class<InnerClassLazySingleton> clazz = InnerClassLazySingleton.class;
            Constructor<InnerClassLazySingleton> c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);//该类构造方法被私有,通过此设置进行强制访问
            InnerClassLazySingleton obj1 = c.newInstance();
            InnerClassLazySingleton obj2 = InnerClassLazySingleton.getInstance();
            System.out.println(obj1==obj2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

InnerClassLazySingleton类的构造方法已经被私有,但还是可以通过反射的方式,直接构造出对象实例,破坏单例。

对比两种方式创建的对象,可以发现结果是false。单例确实被破坏了。

要避免反射破坏单例的漏洞,就可以在构造方法内加入校验:

五、反序列化破坏单例

5.1 通过反序列化破坏单例:

/**
 * @Auther: jesses
 * @Description: 自定义单例类,构造方法私有,提供getInstance方法获取单例实例
 */
public class SeriableSingleton implements Serializable {
    private final static SeriableSingleton INSTANCE = new SeriableSingleton();

    private SeriableSingleton() {
    }

    public static SeriableSingleton getInstance() {
        return INSTANCE;
    }

    //private Object readResolve(){
    //    return INSTANCE;
    //}
}
/**
 * @Auther: jesses
 * @Description: 入口函数,测试通过反序列化得到的多个实例是否单例
 */

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

运行结果: 

可以看到,通过反序列化获取运行得到的不是同一个单例对象。

5.2 如何解决反序列化破坏单例?

将SeriableSingleton类中被注释的readResolve()方法解开,

再次运行test类,

在加上readResolve方法后,test类中两次获取到的是相同的单例对象了。

5.3 原因

现在查看源码 看看为何添加了resolve方法后,就不会创建不同的实例.

点击进入流转对象的readObject()方法:

可以看到调用了readObject0()方法:

继续进入readObject0()方法,可以看到调用了readOrdinaryObject():

进入readOrdinaryObject()方法,可以看到判断了对象是否可以实例化,可以就创建新实例obj:

 进入isInstantiable()方法,得出结论,是根据这个对象是否有构造方法来判断是否可以实例化的。

 有构造方法则true,创建新的实例:

 接着readOrdinaryObject()方法继续向下深入,可以看到源码中判断该对象中是否存在ReadResolve方法,

如果存在该方法,就代理该ReadResolve()方法,将ReadResolve方法的返回 重新赋值给obj对象。

而我们的ReadResolve()方法中直接返回了单例的实例,因此两次创建都是同一个实例:

通过追踪源码,可以了解到实际上还是实例化了两次,只不过有ReadResolve方法时,新创建的对象obj没有返回,而是把单例赋值给了新对象的引用obj。

六、注册式单例

注册式单例又称为登记式单例,就是将每个实例都登记到某处,用唯一的标识获取实例。

注册式单例有两种:枚举登记、容器缓存。

6.1 枚举式单例

自定义枚举类:

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

测试类:

/**
 * @Auther: jesses
 * @Description: 注册式单例-枚举式
 */
public class EnumSingletonTest {

    public static void main(String[] args) {
        try {
            /** 构建该枚举类的实例instance2,并设置属性data为new Object() */
            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setData(new Object());
            
            //将该属性输出到 EnumSingleton.obj 文件
            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();
 
            //读取 EnumSingleton.obj 文件,使用一个新的枚举实例instance1接收
            EnumSingleton instance1 = null;
            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance1 = (EnumSingleton) ois.readObject();
            ois.close();

            //对比两个实例中的data属性
            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData()==instance2.getData());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果,两个实例的data属性相同:

为什么枚举式单例可以避免反序列化破坏单例呢?

接下来,分析源码了解其原理。

使用XJad、Jad等反编译工具对EnumSingleton.class文件进行反编译:

可以看到,枚举式单例实际上在静态代码块中就对INSTANCE进行了初始化,

很显然,这是饿汉式单例,在加载时就初始化实例,因此不会出现线程问题。

接着跟踪反序列化的过程源码,

进入readObject()方法:

其中调用了readObject0()方法,继续深入readObject0方法: 

 在枚举类型的处理逻辑中,调用了readEnum()方法,进入readEnum()方法查看其实现:

 可以看到,通过Enum.valueOf(Class,name)获取了实例,再进入valueOf()方法看看:

终于发现,这里从枚举类的一个enumConstantDirectory通过枚举中的name(值项)获取,

而enumConstantDirectory是一个Map<String,T>的数据结构,

我们知道,Map中的key是不能重复的,因此通过"INSTANCE"这个name只能从改map中获取到同一个对象实例,

这也就是枚举式单例不能被反序列化破坏的原因了:

那么,枚举式单例能否被反射破坏呢?

 测试反射创建枚举式单例的实例:

public class EnumSingletonTest2 {
    public static void main(String[] args) {
        try{
            Class<EnumSingleton> clazz = EnumSingleton.class;
            Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor();
            constructor.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行发现报错,找不到无参构造方法: 

 查看Enum源码,发现Enum只有唯一的一个带参构造方法,并且访问修饰符是protect的。

那么修改代码,再次测试:

public class EnumSingletonTest2 {
    public static void main(String[] args) {
        try{
            Class<EnumSingleton> clazz = EnumSingleton.class;
            Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class,int.class);
            constructor.setAccessible(true);
            EnumSingleton enumSingleton = constructor.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行,发现报错,不能用反射创建枚举式单例的实例:

为什么会限制枚举式代理用反射创建实例呢?

再到源码中寻找原因,进入newInstance()方法:

可以看到,在Constructor.newInstance()方法中做了判断,如果类的修饰符是Enum,就直接抛出异常。 

原来是在JDK层面就已经替我们避免了反射破坏单例。

枚举式单例是《Effective Java》书中推荐的一种单例写法。

因JDK枚举的特性,避免了反射破坏单例,枚举式单例成为较好的一种实现。

6.2 容器式单例

/**
 * @Auther: jesses
 * @Description: 注册式单例-容器缓存
 */
public class ContainerSingleton {

    private ContainerSingleton() {
    }

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

    public static Object getBean(String className) {
        // ConcurrentHashMap是线程安全的,但只能保证map内部线程安全,无法保证此getBean方法线程安全。
        //加synchronized以保证创建单例实例线程安全。
        synchronized (singletonMap) {
            //通过className从Map容器中取实例,若不存在,则使用创建后加入Map容器。
            if (singletonMap.containsKey(className)) {
                return singletonMap.get(className);
            } else {
                Object object = null;
                try {
                    object = Class.forName(className).newInstance();
                    singletonMap.put(className, object);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return object;
            }
        }
    }
    
}
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的,需对获取实例的方法做加锁处理。
容器式单例在Spring中常被使用到,下图为 Spring 中的容器式单例的实现:

七、单例模式小结

单例模式可以保证内存中只有一个实例,减少了内存开销,避免对资源过多占用。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值