单例模式(
Singleton Pattern
)确保一个类只有一个实例,并且提供一个全局的访问。
单例模式随处可见,比如线程池
、缓存
、对话框
、日志对象
等,这些时候如果制造出多个实例,程序运行就会出现预期之外的情况。
这里可能有疑问,我用全局静态变量也能做到一个类只有一个实例,为什么要引入这样一个设计模式呢?原因其实很简单,全局静态变量会造成资源浪费:假设这个类非常消耗资源,程序在运行过程中,不是每一次都用到这个类,那就是极大的浪费。
类图
类图不是目的,仅仅帮助理解
[图片上传失败...(image-1fb5c0-1527174223215)]
单例模式的类图很简单,只有一个类,有一个代表自己实例的instance
变量,还有一个提供全局访问的静态方法getInstance()
。
以下的代码和思路是针对
Java
语言
单例类型
单例模式分为懒汉式和饿汉式,区别在于实例化单例对象的时机。
懒汉式
在懒汉式单例模式实现中,不管单例是否用到,都会实例化一个单例对象。典型的写法如下:
/**
* 单例
* Created by Carlton on 2016/11/21.
*/
class Singleton private constructor()
{
companion object
{
private val instance = Singleton()
fun instance() = instance
}
}
因为
Kotlin
和Java在静态语法上的不一致,后面的代码都用Java
来实现方便理解
/**
* 单例模式
* Created by Carlton on 2016/11/21.
*/
public class Singleton
{
private Singleton()
{
}
private static Singleton instance = new Singleton();
public static Singleton instance()
{
return instance;
}
}
饿汉式非常简单,也不会出现资源占用之外的其他问题,就不多说。
饱汉式、常规方法
饱汉式也就是常规实现方式比较复杂,原因是我们用到类实例的时候才会去实例化,这中间会出现各种各样的情况。
介绍了两种实现方式,接下来我们实现一个常规的单例模式:
/**
* 单例模式
* Created by Carlton on 2016/11/21.
*/
public class Singleton
{
private Singleton()
{
}
private static Singleton instance = null;
public static Singleton instance()
{
if(instance == null)
{
// 1
instance = new Singleton();
}
return instance;
}
}
在客户端获取单例的时候,检查对象是否是null
如果是,则实例化一个,如果不是则直接返回已有的对象,如果在单线程的情况下,确实如此,现在如果,两个或者两个以上的线程就有问题了:
- 如果两个线程都到
1
这个位置 - 那么现在的情况就是
if (instance == null)
判断的时候两个线程都通过了 - 这个时候
instance
会实例化两次,这两个线程拿到的不是同一个实例
怎么解决这个问题呢?不慌解决,先看看一下双重验证和volatile
双重检查和volatile
如果加上线程锁,好像问题就解决了,先看看加线程锁怎么写:
/**
* 单例模式
* Created by Carlton on 2016/11/21.
*/
public class Singleton
{
private Singleton()
{
}
private static Singleton instance = null;
public static Singleton instance()
{
if(instance == null)
{
// 1
synchronized (Singleton.class)
{
// 2
if(instance == null)
{ // 3
instance = new Singleton();
}
}
}
return instance;
}
}
现在看看多线程的情况程序会出现什么问题:
- 如果有两个线程都到了
1
- 因为同步锁的原因,只有一个线程可以先进入到
2
- 当第一个线程进入
3
实例化一个instance
后,第二个线程进入判断的时候,就不会进入3
这就是双重验证,在C/C++
中,这样做是没有问题的,但是:双重检查对Java语言编译器不成立!
原因在于,Java编译器中,Singleton
类的初始化与instance
变量赋值的顺序不可预料,如果一个线程在没有同步化的条件下读取instance
引用,并调用这个对象的方法的话,可能会发现对象的初始化过程还没有完成,从而造成崩溃。
可能有人会觉得volatile
可以解决问题,修改变量申明:
private static volatile Singleton instance = null;
先看看volatile
是什么?
volatile
变量具有synchronized
的可见性特性,但是不具备原子特性。这就是说线程能够自动发现volatile
变量的最新值。volatile
变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。
通过这个描述知道volatile
是一个轻量级的线程同步,之前出现的问题在于线程没有同步化的条件下读取instance
,现在加上volatile
问题就解决了。但是:JDK1.5之前,这样使用双重检查还是有问题。
Java中如何正确的实现单例模式
说了这么多,如何才能正确实现单例模式呢?
- 使用饿汉式
- JDK1.5以后使用带
volatile
修饰的双重检查 - 同步锁加到方法上:
/**
* 单例模式
* Created by Carlton on 2016/11/21.
*/
public class Singleton
{
private static volatile Singleton instance = null;
private Singleton()
{
}
public static synchronized Singleton instance()
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
多说几句,如果把同步锁加到方法上面,代表这个方法同一时间只有一个线程能够进入方法,这个时候后面的线程进入就会正常的直接返回instance
实例。
总结
单例模式在思路上是很简单的模式,也就不提供例子,单例模式还有很多单例模式的变种,但是核心没变:一个类只有一个实例;这个实例由自己来实例化;单例模式没有提供公共的构造函数,所以其他类不能对其实例化。
需要注意的是,这个模式的复杂点在于实现方式,如何才能保证在各种情况下只有一个类实例才是关键点。
不登高山,不知天之高也;不临深溪,不知地之厚也
感谢指点、交流、喜欢
网友评论