单例对象的类只能允许一个实例存在。
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
单例的实现主要是通过以下两个步骤:
- 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
- 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
优点:
- 在内存中只有一个对象,节省内存空间;
- 避免频繁的创建销毁对象,可以提高性能;
- 避免对共享资源的多重占用,简化访问;
- 为整个系统提供一个全局访问点。
缺点:
- 不适用于变化频繁的对象;
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
- 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;
饿汉式(线程安全)
public class HungrySingleton {
// 指向自己实例的私有静态引用,主动创建
private static HungrySingleton hungrySingleton = new HungrySingleton();
// 私有的构造方法
private HungrySingleton() {
}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。
优点:写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
懒汉式(线程不安全)
public class LazySimpleSingleton {
// 指向自己实例的私有静态引用
private static LazySimpleSingleton lazySimpleSingleton;
// 私有的构造方法
private LazySimpleSingleton() {
}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static LazySimpleSingleton getInstance() {
// 被动创建,在真正需要使用时才去创建
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}
单例实例被 Lazy Loading,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
这种写法起到了 Lazy Loading 的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
双重校验锁实现对象单例(线程安全)
public class LazyDoubleCheckSingleton {
// volatile 关键字可以防止 JVM 指令重排优化
private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
// 先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (lazyDoubleCheckSingleton == null) {
// 类对象加锁
synchronized (LazyDoubleCheckSingleton.class) {
// 防止第二次创建实例
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
private static volatile Singleton singleton3;
中的 volatile 必不可少,volatile 关键字可以防止 JVM 指令重排优化。
singleton3 = new Singleton3();
可以分为三步:
-
为 singleton 分配内存空间;
-
初始化 singleton;
-
将 singleton 指向分配的内存空间。
由于 JVM 具有指令重排的特性,执行顺序有可能变成 1-3-2。指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程 T1 执行了 1 和 3,此时 T2 调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。
使用 volatile 会禁止 JVM 指令重排,从而保证在多线程下也能正常执行。