1、模式简介
单例模式在代码中是非常常用的,如线程池、数据库连接、注册表、共享资源、一些非常消耗资源的组件,等等。
单例模式主要解决如下问题:
- 确保一个特殊类的实例是独一无二的;
- 确保这个类的实例非常容易访问(提供了这个类的一个全局访问指针);
以下情况下可以使用单例模式:
- 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时;
- 当要创建的对象非常耗费资源,而且全局用到了很多次时;
- 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。
2、实现方法
2.1、整体思路
单例模式的整体思路如下:
- 在单例类中创建一个私有的本类对象;
- 将单例类的构造方法私有化,即用private修饰;
- 创建一个静态的方法getInstance(),返回本类类型的变量,在这个方法中返回单例。
根据这种思路,单例模式分为饿汉式单例和懒汉式单例。
2.2、饿汉式
饿汉式单例即在声明实例对象的时候就直接创建对象,在需要引用的时候直接引用。
饿汉式单例的简单代码如下:
/**
* 饿汉式单例的单例类
*/
public class HungerSingleton {
// 在声明单例变量的时候直接初始化
private static HungerSingleton instance = new HungerSingleton(); // 私有化构造方法,避免外界直接访问
private HungerSingleton() {
System.out.println("我是饿汉式单例的唯一实例!");
} // 公共的静态方法,用于外界调用该方法来创建唯一的对象
public static HungerSingleton getInstance() {
return instance;
} public void introduce() {
System.out.println("我是introduce()方法,我被调用了!" + Thread.currentThread().getName());
}
}
测试代码:
public class Test {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
HungerSingleton.getInstance().introduce();
}
}).start(); new Thread(new Runnable() {
@Override
public void run() {
HungerSingleton.getInstance().introduce();
}
}).start(); new Thread(new Runnable() {
@Override
public void run() {
HungerSingleton.getInstance().introduce();
}
}).start(); new Thread(new Runnable() {
@Override
public void run() {
HungerSingleton.getInstance().introduce();
}
}).start(); new Thread(new Runnable() {
@Override
public void run() {
HungerSingleton.getInstance().introduce();
}
}).start();
}
}
运行结果:
从代码和运行结果来看,我们在五个线程中分别调用了HungerSingleton.getInstance()方法来创建对象,但HungerSingleton类的构造方法只被调用了一次,也就是说,五次调用只创建了一个对象。可以看出,饿汉式单例不仅保证了只创建一个对象,也保证了线程安全。
综上,饿汉式单例的优点是线程安全的,即无论在多少个线程中调用这个对象,调用的都是事先声明好的;缺点是声明出来的对象可能很占用系统资源,会在调用之前影响系统的工作状态。
2.3、懒汉式
懒汉式单例即在声明的时候不创建对象,在需要引用的时候先创建对象,然后引用。
懒汉式单例的简单代码如下:
public class LazySingleton {
// 声明变量,但不初始化
private static LazySingleton instance; // 私有化构造方法,避免外界直接访问
private LazySingleton() {
System.out.println("我是懒汉式单例类的对象,我是唯一的!");
} // 公共的静态方法,用于外界调用该方法来创建唯一的对象
public static LazySingleton getInstance() {
// 在调用对象的时候判断对象是否存在,如果存在则直接使用,否则才创建对象
if (instance == null) {
instance = new LazySingleton();
}
return instance;
} public void introduce() {
System.out.println("我是introduce()方法,我被调用了!" + Thread.currentThread().getName());
}
}
测试代码和饿汉式单例的测试代码基本相同,也是五个线程同时访问。运行结果如下:
从代码和运行结果来看,当有多个线程同时访问这个单例对象的时候,就有可能创建出多个对象,这不符合单例模式的初衷。为了解决这个方法,我们需要对懒汉式单例进行一定的改进,方法是为创建单例的代码加一层双重检查锁。具体代码如下:
// 公共的静态方法,用于外界调用该方法来创建唯一的对象
public static LazySingleton getInstance() {
// 在调用对象的时候判断对象是否存在,如果存在则直接使用,否则才创建对象
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
再次运行,运行结果如下:
综上,懒汉式单例的优点是在没有调用单例对象之前不会影响系统的工作状态;缺点是线程不安全,即如果在多个线程中同时访问这个单例对象,很可能会创建出多个对象,但这种缺点是可以改进的,方法就是加双重检查锁。
3、模式优缺点
单例模式有以下优点:
- 使用单例模式可以严格的控制用户怎样以及如何访问它;
- 节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式可以提高系统的性能;
- 允许可变数目的实例。
单例模式有以下缺点:
- 单例模式的扩展有很大的困难;
- 单例类职责过重,在一定程度上违背了“单一职责原则”;
- 滥用单例模式可能产生一些负面问题,如一个对象的访问过多可能引起对象溢出;如果实例化的对象长时间不被使用,系统会默认为是垃圾而回收,导致对象状态的丢失。
4、多实例单例
前面说过,单例可以控制生成的对象的个数或不同种类的对象。例如,一个单例类可能会有多个子类,这些子类都是对父类的扩展,每个子类都有其独特的作用,那么我们可能就会控制每个子类都可以有一个单例对象,这时就用到了多实例单例。
其实想想也不奇怪,我们既然能控制一个类只能生成一个对象,那么控制这个类生成固定个数个对象自然也不成问题。要解决这个问题,我们需要用到集合类,Map、List都可以,每当需要创建一个对象的时候,我们就遍历这个集合,判断这个对象在集合中有没有单例,如果有,则直接调用集合中的对象;如果没有,才可以创建一个对象然后存到集合中。
当然,这种多实例单例的方法也不仅用在每个类只能创建一个实例的情况,也可以用在一个类可以创建多个相同的实例的情况,其解决方法依然是使用集合,具体方法如上,就不多说了。
最后贴出单例模式的GitHub地址:【GitHub - Singleton】。