单例模式是一种常用的设计模式,该模式提供了一种创建对象的方法,确保在程序中一个类最多只有一个实例。
单例有什么用处?
有一些对象其实我们只需要一个,比如线程池、缓存、对话框、处理偏好设置和注册表的对象、日志对象,充当打印机、显示等设备的驱动程序对象。其实,这类对象只能有一个实例,如果制造出来多个实例,就会导致许多问题,如:程序的行为异常、资源使用过量,或者是不一致的结果。
Singleton通常用来代表那些本质上唯一的系统组件,比如窗口管理器或者文件系统。
在Java中实现单例模式,需要一个静态变量、一个静态方法和私有的构造器。
经典的单例模式实现
对于一个简单的单例模式,可以这样实现:
- 定义一个私有的静态变量uniqueInstance;
- 定义私有的构造方法。这样别处的代码无法通过调用该类的构造函数来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例;
- 提供一个getInstance()方法,该方法中判断是否已经存在该类的实例,如果存在直接返回,不存在则新建一个再返回。代码如下:
public class Singleton{
private static Singleton uniqueInstance;//私有静态变量
//私有的构造器。这样别处的代码无法通过调用该类的构造函数来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
private Singleton(){}
//静态方法
public static Singleton getInstance(){
//如果不存在,利用私有构造器产生一个Singleton实例并赋值到uniqueInstance静态变量中。
//如果我们不需要这个实例,他就永远不会产生。这叫做“延迟实例化(懒加载)“
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
public class Singleton{
private static Singleton uniqueInstance;//私有静态变量
//私有的构造器。这样别处的代码无法通过调用该类的构造函数来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
private Singleton(){}
//静态方法
public static Singleton getInstance(){
//如果不存在,利用私有构造器产生一个Singleton实例并赋值到uniqueInstance静态变量中。
//如果我们不需要这个实例,他就永远不会产生。这叫做“延迟实例化(懒加载)“
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
这段代码使用了延迟实例化,在单线程中没有任何问题。但是在多线程环境下,当有多个线程并行调用 getInstance(),都认为uniqueInstance为null的时候,就会调用uniqueInstance = new Singleton();
,这样就会创建多个Singleton实例,无法保证单例。
解决多线程环境下的线程安全问题,主要有以下几种写法:
同步getInstance()方法
关键字synchronized可以保证在他同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。
同步getInstance()方法是处理多线程最直接的做法。只要把getInstance()变成同步(synchronized)方法,就可以解决并发问题了。
public class Singleton{
private static Singleton uniqueInstance;//私有静态变量
//私有构造器
private Singleton() {}
//synchronized同步方法
public static synchronized Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
public class Singleton{
private static Singleton uniqueInstance;//私有静态变量
//私有构造器
private Singleton() {}
//synchronized同步方法
public static synchronized Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
但是,同步的效率低,会降低性能。只有第一次执行此方法的时候,才真正需要同步。也就是说,一旦设置好uniqueInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种累赘。同步getInstance()方法既简单又有效。如果说对性能要求不高,这样就可以满足要求。
“急切”实例化
之前的实现采用的是懒加载方式,也就是说,当真正用到的时候才会创建;如果没被使用到,就一直不会创建。
懒加载方式在第一次使用的时候, 需要进行初始化操作,可能会比较耗时。
如果确定一个对象一定会使用的话,可以采用“急切”地实例化,事先准备好这个对象,需要的时候直接使用就行了。这种方式也叫做饿汉模式。具体代码:
public class Singleton{
//在静态初始化器中创建单例,保证了线程安全性
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance(){
return uniqueInstance;
}
}
public class Singleton{
//在静态初始化器中创建单例,保证了线程安全性
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance(){
return uniqueInstance;
}
}
饿汉模式是如何保证线程安全的?
饿汉模式中的静态变量是随着类加载时被初始化的。static关键字保证了该变量是类级别的,也就是说这个类被加载的时候被初始化一次。注意与对象级别和方法级别进行区分。
因为类的初始化是由类加载器完成的,这其实是利用了类加载器的线程安全机制。类加载器的loadClass方法在加载类的时候使用了synchronized关键字。也正是因为这样, 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)。
双重检查加锁
杀鸡用牛刀。实现单例模式可以利用双重检查加锁(double-checked locking),首先检查是否实例已经创建了,如果尚未创建,“才”进行同步。这样,只有第一次会同步。
public class Singleton{
//使用volatile关键字,确保当uniqueInstance变量被初始化成为Singleton实例时,多线程可以正确地处理uniqueInstance变量。
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if(uniqueInstance == null){//第一次检查
synchronized(Singleton.class){
if(uniqueInstance == null){//第二次检查
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
public class Singleton{
//使用volatile关键字,确保当uniqueInstance变量被初始化成为Singleton实例时,多线程可以正确地处理uniqueInstance变量。
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if(uniqueInstance == null){//第一次检查
synchronized(Singleton.class){
if(uniqueInstance == null){//第二次检查
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
如果性能是关注的重点,双重检查加锁可以大幅减少getInstance()的时间消耗成本。
在Java 1.5发行版本之前,双重检查模式的功能很不稳定,因为volatile修饰符的语义不够强,难以支持它。Java 1.5发行版本中引入的内存模式解决了这个问题,如今,双重检查模式是延迟初始化的一个实例域的方法。
为什么要进行双重检查?只检查一次不行吗?
解答:只检查一次不行。只检查一次的代码如下:
if(uniqueInstance == null){//第一次检查
synchronized(Singleton.class){
uniqueInstance = new Singleton();
}
}
当两个线程同时判断uniqueInstance == null的时候,都会去获得Singleton.class的锁对象,由于两个线程拥有的锁对象是同一个Singleton.class,两个线程先后执行,也就是两个线程都会进入同步代码块创建一个新的对象,造成返回的uniqueInstance 并不是唯一的,这样也就不符合单例模式了。
最佳方法
从Java 1.5发行版本起,实现Singleton只需要编写一个包含单个元素的枚举类型:
public enum Singleton {
INSTANCE;
}
public enum Singleton {
INSTANCE;
}
使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。注意:如果Singleton必须拓展一个超类,而不是扩展Enum的时候,则不宜使用这个方法。