一、单例模式的作用
通过单例模式可以保证系统中一个类只有一个实例
二、应用场景
主要应用在资源共享控制资源之间交流
- 数据库连接池
- 日志应用
- 应用配置
- 线程池
三、分类
1. 饿汉式
- 定义变量的时候就直接初始化
- 但容易浪费空间
//饿汉式
public class Hungry {
//饿汉式可能会浪费空间
private Hungry()
{
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance()
{
return HUNGRY;
}
}
2. 静态内部类
- 通过静态内部类实现单例,不会在一开始就初始化,而是在getInstance()被调用的时候再初始化对象
- 但无论是静态内部类还是饿汉式,用反射都可以获取私有构造器而破坏单例
public class Holder {
private Holder()
{
}
public static Holder getInstance()
{
return InnerClass.instance;
}
private static class InnerClass
{
private static final Holder instance = new Holder();
}
}
//通过反射获取构造器创建两个对象
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Constructor<Holder> constructor = Holder.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Holder instance1 = constructor.newInstance(null);
Holder instance2 = constructor.newInstance(null);
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
输出:
804564176
1421795058
两个对象的hashCode不同,说明创建了两个不同的对象,反射可以破坏静态内部类模式的单例
3. 懒汉式
- 定义static变量存储实例
- 定义getInstance()方法来创建并获取实例
- 避免了一开始就完成初始化
//懒汉式
public class LazyMan {
private LazyMan()
{
synchronized (LazyMan.class) {
if(lazyMan!=null)
{
throw new RuntimeException("do not try to use refection");
}
}
}
private volatile static LazyMan lazyMan;
//双重检测模式的懒汉式单例,DCL懒汉式
public static LazyMan getInstance()
{
if(lazyMan==null)
{
synchronized (LazyMan.class) {
if(lazyMan==null)
{
System.out.println("new instance");
lazyMan = new LazyMan();
/**
* new 的过程
* 1. 分配内存空间
* 2. 执行构造方法,初始化对象
* 3. 把这个对象指向这个空间
*
* 但是实际执行顺序可能会不一样比如132
* 在LazyMan前加上volatile关键字,保证123的执行顺序
* volatile关键字保证了变量先写后读的顺序
*/
}
}
}
return lazyMan;
}
}
- 测试1: 创建两个实例
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
LazyMan instance = LazyMan.getInstance();
LazyMan instance2 = LazyMan.getInstance();
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
输出:
new instance
804564176
804564176
由于懒汉式的双重检测,只有一个对象被创建出来,main中的两个实例是同一个对象
- 测试2: 第二个对象通过反射来创建
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
LazyMan instance = LazyMan.getInstance();
//反射通过构造器创建对象
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance2 = constructor.newInstance();
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
输出:
程序报错,源自LazyMan构造器中抛出的RuntimeException,阻止了反射来创建新对象
- 测试3: 两个对象都用反射来创建
//多线程并发
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
//反射会破坏单例
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance = constructor.newInstance();
LazyMan instance2 = constructor.newInstance();
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
输出:
804564176
1421795058
两个对象的hashCode不一致,说明两个对象不是同一个,因此这里反射破坏了单例,但我们可以用枚举类来解决这个问题,因为在枚举类不允许反射来干预对象的创建
4. 枚举
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance()
{
return INSTANCE;
}
}
//通过反射试图创建两个对象
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingle instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
输出:
程序报错,不允许通过反射创建枚举类实例,因此使用枚举实现单例可以防止反射破坏
四、总结
- 饿汉式一开始就初始化了实例,容易浪费空间资源
- 静态内部类式避免了一开始就初始化
- 饿汉式和静态内部类式在多线程环境下都是线程安全的,但它们都会被反射所破坏
- 懒汉式如果不加双重检测是线程不安全的,必须加上双重检测模式才能在多线程并发下保证单例
- 懒汉式终究无法完全防止反射的破坏,但枚举实现单例可以完全防止反射破坏