单例模式属于创建型模式, 是 Java 中最简单的设计模式之一, 它提供了一种创建对象的最佳方式,
这种模式涉及到一个单一的类, 该类负责创建自己的对象, 同事确保只有单个对象被创建, 提供唯一对象的创建方式, 可以直接访问, 不需要实例化该类的对象
注意:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给其他所有对象提供这一实例
- 需要生成唯一序列的环境
- 需要频繁实例化然后销毁的对象
- 创建对象时耗时过多或者耗资源过多, 但是有经常用到的对象
- 方便资源互相通信的环境
优点:
- 在内存中只有一个对象, 节省内存空间;
- 避免频繁的创建销毁对象, 可以提高性能;
- 避免对共享资源的多重占用, 简化访问;
- 为整个系统提供一个全局访问点.
缺点:
- 不适用于变化频繁的对象;
- 滥用单例模式带来的一些问题, 如为了节省资源将数据库连接池对象设计为单例, 可能会导致共享连接池对象的程序过多而出现连接池溢出;
- 如果实例化的对象长时间不被利用, 系统会认为该独享是垃圾而被回收, 可能导致对象状态的丢失;
像不安全的懒汉式一样, 这种方式也具有很好的懒加载, 适用于多线程, 但是效率很低
优点: 第一次调用才会初始化, 避免内存浪费
缺点: 必须加锁才能保证单例, 加锁会影响性能
public class Singleton2 {
private static Singleton2 singleton2;
private Singleton2() {}
// synchronized 保证线程安全
public synchronized static Singleton2 getSingleton2() {
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}
这种方式比较常用, 但是容易产生垃圾对象, 饿汉式是线程安全的, 不是懒加载的
优点: 没有加锁, 执行效率会高
缺点: 类加载的时候就初始化, 浪费内存
懒汉式基于 classloader 机制避免了多线程的同步问题, 不过, instance 在类加载的时候就实例化了
public class Singleton3 {
private static Singleton3 singleton3 = new Singleton3();
private Singleton3() {}
public static Singleton3 getSingleton3() {
return singleton3;
}
}
要求 jdk 必须大于 1.5, 这种方式是懒加载 且 线程安全的
这种方式采用双锁机制, 安全且在多线程的情况下能保持高性能
public class Singleton4 {
/**
* jdk1.5 及以上, volatile 可以防止 cpu 重排序, 如果不加 volatile 在理论上可能会出现获取到空对象的情况, 原因如下:
*
* 在Java中,对象的创建大致分为以下几个步骤:
* 1.分配内存
* 2. 初始化对象(调用构造函数)
* 3. 将对象的引用指向内存地址
*
* 在多线程环境中,如果不进行适当的同步,CPU的重排序可能导致线程看到的对象状态不一致。在对象创建过程中,CPU重排序可能发生在以下步骤:
* 1. 分配内存
* 2. 将对象引用指向内存地址
* 3. 初始化对象
*/
private volatile static Singleton4 singleton4;
private Singleton4() {}
public static Singleton4 getSingleton4() {
if(singleton4 == null){
synchronized (Singleton4.class) {
if (singleton4 == null) {
singleton4 = new Singleton4();
}
}
}
return singleton4;
}
}
这种方式不仅是懒加载的, 而且是线程安全的, 这种方式能达到双检锁方式一样的效果, 但是实现起来更简单。
这种方式只适用于静态域的情况, 双检锁的方式可以在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 饿汉式式 不同的是:第 饿汉式式 只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class Singleton5 {
private Singleton5() {
}
private static class SingletonHolder {
static final Singleton5 INSTANCE = new Singleton5();
}
public static final Singleton5 getInstance() {
return SingletonHolder.INSTANCE;
}
public static void main(String[] args) {
Singleton5Runnable singleton5Runnable = new Singleton5Runnable();
new Thread(singleton5Runnable).start();
}
}
要求 jdk1.5 及以上, 不是懒加载的, 是多线程安全的
这种方式没有被广泛采用, 但是这是实现单例模式的最佳方式, 它更简洁, 这种方式是 《Effective Java》作者Josh Bloch 提倡的方式, 它不仅仅能够避免多线程同步问题, 而且还自动支持序列机制, 防止反序列化重新创建的对象, 绝对防止多次实例化
public enum Singleton6 {
instance;
private EnumResource enumResource;
private Singleton6() {
enumResource = new EnumResource();
}
public EnumResource getInstance() {
return enumResource;
}
}
class EnumResource {
public static void main(String[] args) {
System.out.println(Singleton6.instance.getInstance());
System.out.println(Singleton6.instance.getInstance());
System.out.println(Singleton6.instance.getInstance());
}
}
一般情况下,不建议使用 懒汉方式, 建议使用 饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用 登记方式。如果涉及到反序列化创建对象时,可以尝试使用 枚举方式。如果有其他特殊的需求,可以考虑使用 双检锁方式。