单例模式保证了系统内存中该类只有一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能

使用场景:需要频繁创建和销毁的对象、创建对象耗时较多、耗费资源过多但又经常用到的对象、工具类对象、频繁访问数据库和文件的对象(比如数据源或session工厂)

饿汉式

优点:在类装载的时候就完成了实例化,避免了线程同步的问题

缺点:在类装载的时候就完成了实例化,没有达到Lazy Loading的效果,如果至始至终没有用到这个实例,那么会造成内存的浪费

静态常量方方式创建

1、构造器私有化(防止通过 new 创建一个对象实例)

2、类的内部创建对象

3、向外暴露一个静态的公共方法,通过该方法返回该类的对象实例(getInstance)


静态代码块方式创建

与上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的,可能造成内存浪费


枚举方式创建

enum类的本质其实是一个final修饰的类,因为枚举的性质,INSTANCE 又是由final static修饰的,所以我们用enum来创建单例。而JVM来管理这个实例,也保证了线程安全,推荐使用


反射破坏单例

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Exception {
reflection(Singleton.class);
}
private static void reflection(Class<?> clazz)
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<?> constructor = clazz.getDeclaredConstructor();
//让私有的构造方法也可以被使用
constructor.setAccessible(true);
System.out.println("反射创建实例:" + constructor.newInstance());
}

为了预防这种情况,可以在构造方法内进行判断


反序列化破坏单例

当实现了Serializable接口时,可以通过反序列化的方式破坏单例

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Exception {
serializable(Singleton.getInstance());
}
private static void serializable(Object instance) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("反序列化创建实例:" + ois.readObject());
}

为了预防这种情况,可以重写readResolve方法


unsave破坏单例

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {
unsave(Singleton.class);
}
private static void unsafe(Class<?> clazz) throws InstantiationException {
Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
System.out.println("Unsafe 创建实例:" + o);
}

懒汉式

线程不安全方式创建

起到了 Lazy Loading 的效果,但是只能在单线程下使用

如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还没来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式

注意:在实际开发中,不要使用这种方式


同步方法的方式创建

解决了线程安全问题。但是效率太低,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低


同步代码块的方式创建(x)

并不能起到线程同步的作用,实际开发中,不能使用这种方式


双检锁的方式创建

进行了两次 if (singleton == null)检查,保证线程安全。而且实例化代码只用执行一次,后面再次访问时,判断 singleton == null,直接 return 实例化对象,也避免的反复进行方法同步

线程安全;延迟加载;效率较高

volatile的作用:

INSTANCE = new Singleton() 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造

如果线程1 先执行了赋值,线程2 执行到第一个 INSTANCE == null 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象


内部类方式创建

根据内部类的特性,仅有在调用时才会加载类

1
2
3
4
5
6
7
8
9
10
11
public class Singleton implements Serializable {
private Singleton() {}

private static class Holder {
static Singleton INSTANCE = new Singleton5();
}

public static Singleton getInstance() {
return Holder.INSTANCE;
}
}

jdk中的体现

在jdk中应用,java.lang.Runtime就利用了单例模式

每一个java应用程序都有一个Runtime实例。Runtime的单例模式是采用饿汉模式创建的,当你加载这个类文件时,这个实例就已经存在

1