单例模式
单例模式,对大多数人来说应该是最为熟悉,也最容易理解的一种设计模式。所谓单例,顾名思义就是指一个类只有一个实例。单例模式应用很广,例如Spring框架的核心技术IoC注入,就会涉及到Bean是否为单例。
最近写一个基于JavaFX的GUI项目的时候,在调试时发现总是抛出Exception in thread "JavaFX Application Thread" java.lang.OutOfMemoryError: Java heap space
的异常。我们知道,JVM的Heap堆用于存储对象实例。Heap内存溢出的原因可能是因为程序在频繁地创建、销毁对象。
有了单例模式,我们就可以避免大量地创建对象、减少Heap堆内存的占用。
在正式介绍单例模式之前,我们先来回顾一下对象的实例化过程。
Person person = new Person();
当遇到new指令时,JVM会执行以下步骤:
- 判断该类是否已经被加载过。如果没有就会执行类加载过程
ClassLoader
将.class
文件加载到内存- 执行静态代码块和静态初始化语句
- 为新生对象分配一块内存空间。
- 创建一个空白对象,也就是将对象的所有成员变量设置为默认值。
(Tips:基本数据类型有对应的默认值,而引用数据类型的默认值为null
。这也是成员变量可以不用赋初值就可使用的原因) - 子类调用父类的构造方法
- 调用自身的构造方法
明白对象的初始化过程对于我们了解每种单例模式的方式是非常有帮助的。下面,我们终于要进入正题了!
1.饿汉模式
public class EagerSingleton {
private static EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {
System.out.println("Construct EagerSingleton ....");
}
public static EagerSingleton getInstance() {
System.out.println("In Method : getInstance");
return instance;
}
}
我们在类的内部声明一个私有的静态成员变量,且在声明时为其赋值,那么在类装载的时候就会完成EagerSingleton
的实例化( 执行 new EagerSingleton()
)
@Test
public void test01() {
EagerSingleton eagerSingleton = EagerSingleton.getInstance();
}
执行上面的代码控制台输出如下:
Construct EagerSingleton ...
In Method : getInstance
这种方法简单而且避免了线程同步问题,不必担心出现创建多个实例的情况。但是它也存在一些问题:在装载类的时候就完成了实例化。假如从始至终都没有使用这个实例,就会造成内存的浪费,没有达到 Lazy Loading 的效果。
2.懒汉模式
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
System.out.println("New EagerSingleton ....");
}
//加锁,保证线程安全
public static synchronized LazySingleton getInstance() {
System.out.println("In Method : getInstance");
if (instance == null) {
System.out.println("Now instance is null ...");
instance = new LazySingleton();
}
return instance;
}
}
这种方式同样声明一个类变量,但初值为null
,等到调用getIntance()
方法时,才执行new LazySingleton()
进行实例化。此外,为了避免多个线程同时访问代码块,造成创建多个实例的情况,我们给getInstance()
方法加了synchronized
,使得在同一时间,方法只能被一个线程访问。
第一次调用getInstance()
方法控制台输出如下:
In Method : getInstance
Now instance is null ...
New EagerSingleton ....
懒汉模式在需要实例的时候才会创建实例,实现了懒加载,且使用synchronized
保证了线程安全。但是,每次调用getInstance()
都要去获取锁,获取和释放锁的过程很耗时,影响了程序的性能和效率。
3.另一种加锁的懒汉模式(?)
public class LazySingletonP {
private static LazySingletonP singletonP;
private LazySingletonP() {
System.out.println("New LazySingletonP ...");
}
public static LazySingletonP getInstance(){
System.out.println("In Method getInstance ...");
if (singletonP == null){
synchronized (LazySingletonP.class){
System.out.println("Now instance is null ...");
singletonP = new LazySingletonP();
}
}
return singletonP;
}
}
这种方法在第二种方式基础上做了改进,不再对方法进行线程同步,而是改为对类的资源做线程同步。这样一来,只有在我们需要创建实例时才会进行获取锁、释放锁的过程,提高了效率。BTW,思考一下,这样真的可以保证线程安全么?想象一下:假如有两个线程先后通过了if
语句(第一个线程还没有执行完时,第二个执行到if
,判定singletoonP==null
,通过)第一个线程拿到了类资源,第二个线程等待;第一个线程创建实例,释放资源后,第二个接着也会拿到类的资源,再重新new一个实例。由此可见,这种方式在多线程下是不能保证单例的,故摒弃该方式。
4.双重检查
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton singleton;
private DoubleCheckSingleton() { }
public static DoubleCheckSingleton getInstance(){
if(singleton == null){
synchronized (DoubleCheckSingleton.class){
if (singleton == null){
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
双重检查方式,在第三种方法上做了改进。既然可能有多个线程进入if
语句内部,我们不妨在内部再加一重判断,即使后面的线程拿到了锁,只要前面的线程已经创建了实例,后面的线程也就不执行new
指令了。这样就能达到单例的目的。对于静态变量,应该使用volatile
关键字进行修饰,保证当一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
5.静态内部类
public class Singleton {
private Singleton() {
System.out.println("New Singleton ...");
}
public static Singleton getInstance() {
System.out.println("In Method ... getInstance ");
return Singleton.SingletonHolder.instance;
}
// 静态内部类 SingletonHolder
private static class SingletonHolder {
private static Singleton instance = new Singleton();
private SingletonHolder() {
System.out.println("New Inner Class ...");
}
}
}
这种方式与饿汉方式类似,只不过在此借助一个静态内部类 SingletonHolder
来进行实例化操作。
当装载Singleton类的时候,SingleHolder
不会立即加载。只有我们需要实例的时候,调用getInstance()
,才会装载SingleHolder
类。我们来看一下控制台的输出:
In Method ... getInstance
New Singleton ...
并没有输出New Inner Class ...
,可见JVM只是加载了静态内部类的类信息,并没有对内部类进行实例化。由于这一过程是由JVM完成的,它帮助我们确保了线程安全性,同时也达到了懒加载的目的。
(占坑,未完待续……)