为什么来谈谈单例模式
单例模式,作为设计模式中看起来最简单的一种,在面试中经常被问到,同时还会要求手写单例模式的代码。故觉得有必要自己记录一下对单例模式的学习心得。
单例模式是什么
单例模式是一种对象创建模式,它用于产生一个对象的具体实例,并确保系统中有且只有该对象的实例。
单例模式创建出来的单例,适用于比如说:线程池、缓存、日志对象等等,事实上有些对象确实只能有一个实例,多了反而会引起问题。站在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一下哦。