您的位置:首页 > 其它

设计模式:如何优雅地手写单例模式

2020-01-13 00:29 218 查看

单例模式是一种常用的设计模式,该模式提供了一种创建对象的方法,确保在程序中一个类最多只有一个实例。

单例有什么用处?

有一些对象其实我们只需要一个,比如线程池、缓存、对话框、处理偏好设置和注册表的对象、日志对象,充当打印机、显示等设备的驱动程序对象。其实,这类对象只能有一个实例,如果制造出来多个实例,就会导致许多问题,如:程序的行为异常、资源使用过量,或者是不一致的结果。

Singleton通常用来代表那些本质上唯一的系统组件,比如窗口管理器或者文件系统。

在Java中实现单例模式,需要一个静态变量、一个静态方法和私有的构造器。

经典的单例模式实现

对于一个简单的单例模式,可以这样实现:

  1. 定义一个私有的静态变量uniqueInstance;
  2. 定义私有的构造方法。这样别处的代码无法通过调用该类的构造函数来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例;

  3. 提供一个getInstance()方法,该方法中判断是否已经存在该类的实例,如果存在直接返回,不存在则新建一个再返回。代码如下:

public class Singleton{
private static Singleton uniqueInstance;//私有静态变量

//私有的构造器。这样别处的代码无法通过调用该类的构造函数来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
private Singleton(){}

//静态方法
public static Singleton getInstance(){
//如果不存在,利用私有构造器产生一个Singleton实例并赋值到uniqueInstance静态变量中。
//如果我们不需要这个实例,他就永远不会产生。这叫做“延迟实例化(懒加载)“
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
[/code]

这段代码使用了延迟实例化,在单线程中没有任何问题。但是在多线程环境下,当有多个线程并行调用 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;
}
}
[/code]

但是,同步的效率低,会降低性能。只有第一次执行此方法的时候,才真正需要同步。也就是说,一旦设置好uniqueInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种累赘。同步getInstance()方法既简单又有效。如果说对性能要求不高,这样就可以满足要求。

“急切”实例化

之前的实现采用的是懒加载方式,也就是说,当真正用到的时候才会创建;如果没被使用到,就一直不会创建。

懒加载方式在第一次使用的时候, 需要进行初始化操作,可能会比较耗时。

如果确定一个对象一定会使用的话,可以采用“急切”地实例化,事先准备好这个对象,需要的时候直接使用就行了。这种方式也叫做饿汉模式。具体代码:

public class Singleton{
//在静态初始化器中创建单例,保证了线程安全性
private static Singleton uniqueInstance = new Singleton();

private Singleton() {}

public static Singleton getInstance(){
return uniqueInstance;
}
}
[/code]

饿汉模式是如何保证线程安全的?

饿汉模式中的静态变量是随着类加载时被初始化的。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;
}

}
[/code]

如果性能是关注的重点,双重检查加锁可以大幅减少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;
}
[/code]

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。注意:如果Singleton必须拓展一个超类,而不是扩展Enum的时候,则不宜使用这个方法。

参考

  1. Eric Freeman;ElElisabeth Freeman.HeadFirst设计模式[M]. 北京:中国电力出版社, 2007.
  2. Joshua Bloch.Effective Java中文版(原书第3版)[M]. 北京:机械工业出版社, 2018.
  3. 漫话:如何给女朋友解释什么是单例模式?

转载于:https://www.cnblogs.com/sgh1023/p/10752592.html

  • 点赞
  • 收藏
  • 分享
  • 文章举报
baobo2427 发布了0 篇原创文章 · 获赞 0 · 访问量 153 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐