Java设计模式之单例模式

为什么来谈谈单例模式

单例模式,作为设计模式中看起来最简单的一种,在面试中经常被问到,同时还会要求手写单例模式的代码。故觉得有必要自己记录一下对单例模式的学习心得。

单例模式是什么

单例模式是一种对象创建模式,它用于产生一个对象的具体实例,并确保系统中有且只有该对象的实例。
单例模式创建出来的单例,适用于比如说:线程池、缓存、日志对象等等,事实上有些对象确实只能有一个实例,多了反而会引起问题。站在JVM的角度来说,人家才不想每次用的时候都去给你创建一个实例,使用单例模式可以节省创建对象的时间。

单例模式的特点

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

单例模式的诸多问题

总有好事者会有些那些疑问,比如说面试官。下面我给出一些我遇到的问题及我的答案(关于答案欢迎拍砖!!!)
:你怎么去保证别人不去new一个你的单例?:你new不了,单例模式没有公开的构造方法,它的构造方法是声明成private的。
再问:那单例模式自己是怎么实例化的?:单例有个静态方法,你要用我的实例,就要调用这个静态方法。至于这个实例是你调用方法才生成的还是一开始就有的,这就分出了单例模式最基本的两种实现方式,即懒汉式(也叫延迟实例化lazy instantiaze)和饿汉式(eagerly instantiaze)。
还有人问:单例模式和静态类你怎么看?:这个问题我觉得是见仁见智的,两者都能用于解决类似的问题,我们要分场景使用,如果你的单例只提供全局访问但是不需要记录状态就选用静态类。在我们的jdk中同样也有这样的例子:Math就是一个静态类,它让构造函数标记为私有,所以你无法创建Math的实例,但它给我们提供了很多静态方法,也能用于全局访问。而Runtime则就是一个单例的类,该类的注释上解释得很清楚 “Every Java application has a single instance of class”。单例模式的目的只有一个:确保类只有一个实例并提供全局访问。
更有人问:单例模式和全局变量的区别?:全局变量说白了是对一个对象的静态引用。在程序一开始的时候就加载好了,而单例模式可以做到延迟加载。全局变量做不到确保只有一个实例。单例模式可以控制单例数量;可以进行有意义的派生;对实例的创建有更自由的控制。
最后一个问题请你手写一个单例模式。

单例模式的代码实现

经典单例模式

经典单例模式也就是我们上面说的懒汉式单例,直接看代码:

package Singleton;

/**
 * Classic Singleton Model
 * lazy instantiaze
 * Created by gray on 2017/3/25.
 */
public class ClassicSingleton {
    private static ClassicSingleton instance;
    private ClassicSingleton(){}
    // 静态的工厂方法
    public static ClassicSingleton getInstance(){
        if (instance == null) {
            instance = new ClassicSingleton();
        }
        return instance;
    }
}

这种单例模式在单线程下目测没什么毛病,但是到了多线程下就会有不一样的情况发生了。
这里写图片描述
由此可见它不是线程安全的代码。console打印出来的是对象的hashCode,可以明显看出出现了不一样的结果。那我们将这个单例模式改进一下。

改进单例模式

改进的单例模式是我们上面说的饿汉式,它依赖JVM在加载这个类的时候就马上创建了实例,JVM帮我们保证了线程安全。

package Singleton;

/**
 * Improved Singleton Mode
 * eagerly instantiaze
 * To ensure thread safety
 * Created by gray on 2017/3/25.
 */
public class ImprovedSingleton {
    private static final ImprovedSingleton instance = new ImprovedSingleton();
    private ImprovedSingleton(){}
    public static ImprovedSingleton getInstance(){
        return instance;
    }
}

当然还有既简单又有效的方法,把懒汉式的getInstance()变成同步的方法就解决了,如果程序可以接受同步所带来的额外负担那就这样改就好了。
如果注重性能,我们还有别的办法,双重检查加锁,在getInstance()中减少使用同步。

package Singleton;

/**
 * double-checked locking
 * To ensure thread safety
 * Created by gary on 2017/3/25.
 */
public class DoubleCheckedLockingSingleton {
    private volatile static DoubleCheckedLockingSingleton instance;
    private DoubleCheckedLockingSingleton(){}
    public static  DoubleCheckedLockingSingleton getInstance(){
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class){
                if (instance == null) {
                   instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}

还有一种写法,使用静态内部类,既实现了线程安全,又避免了同步带来的性能影响。

package Singleton;

/**
 * use static Inner Class
 * Created by gray on 2017/3/25.
 */
public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton(){}
    private static class InnerClass {
        private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }
    public static final StaticInnerClassSingleton getInstance() {
        return InnerClass.instance;
    }
}

以上的这些改写我们在使用中选择合适自己的方案来实现单例。我们必须悲观地认为所有程序都是多线程的。
在《Effective Java》一书中作者力推另一种实现方法。书中写到:从Java 1.5发行版本起,实现Singleton还有第三种方法。只需编写一个包含单个元素的枚举类型。这种方法在功能上与公有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛使用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

package Singleton;

/**
 * Effective Java Recommended method
 * Created by gray on 2017/3/25.
 */
public enum  SingletonByEnum {
    INSTANCE;
    public static SingletonByEnum getInstance(){
        return  INSTANCE;
    }
}

书中提到了反射攻击,单例模式的私有构造方法保证了单例的全局唯一性,但是可以通过反射机制调用私有的构造方法去实例化。
下面我们来试试反射攻击

    // 反射攻击测试 饿汉单例为例
    public static void attack(){
        Class<?> cls = ImprovedSingleton.class;
        try {
            Constructor<?> declaredConstructor = cls.getDeclaredConstructor(null); // 无参构造函数
            declaredConstructor.setAccessible(true);
            ImprovedSingleton instance = (ImprovedSingleton)declaredConstructor.newInstance();
            ImprovedSingleton instance1 = ImprovedSingleton.getInstance();
            System.out.println("反射攻击结果:"+instance.hashCode());
            System.out.println("正常调用结果:"+instance1.hashCode());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

测试结果如图所示:
这里写图片描述
打印的同样是hashCode,可以看出确实出现了两个不一样的实例。
对于这种情况书中给出的解决方案是:如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。
修改过后的单例模式

package Singleton;

/**
 * Created by gray on 2017/3/26.
 */
public class SingletonNotAttackByReflect {
    private static boolean flag = false;
    private static final SingletonNotAttackByReflect instance = new SingletonNotAttackByReflect();
    private SingletonNotAttackByReflect(){
        // 修改构造器,抵御通过反射机制调用私有构造函数
        synchronized (SingletonNotAttackByReflect.class){
            if (flag == false) {
                flag = !flag;
            } else { // 非法调用构造函数,抛出异常
                throw new RuntimeException("You can only call the constructor once!!!");
            }
        }
    }
    public static SingletonNotAttackByReflect getInstance(){
        return instance;
    }
}

再进行测试,结果如图所示
这里写图片描述
反射攻击失败。
单例模式的学习笔记到这就差不多了,最后附上:单例模式博客中的所有代码级测试代码,需要的朋友可以自取,觉得还可以的话star一下哦。

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值