一、什么是单例模式(Singleton Pattern)
java中单例模式是一种常见的设计模式,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。单例模式只是实例化一次,之后可以直接访问该唯一的对象。
单例模式的特点:
1. 单例类只能有一个实例;
2. 单例类必须自己创建自己的唯一实例;
3. 单例 类必须给所有其他对象提供这一实例
单例模式的意图:
保证只有一个实例,并提供一个访问它的全局访问点
单例模式主要解决的问题:
主要解决一个全局使用的类频繁的创建和销毁。
单例模式何时使用:
控制实例数量,节省资源。
单例模式如何解决这个问题:
判断系统中是否有这个单例,如果有则返回,没有则创建。
单例模式关键点:
构造函数的私有性
单例模式的应用场景:
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例
二、单例模式几种写法
1.懒汉式单例
public class Singleton {
//静态实例
private static Singleton instance ;
//私有的构造方法
private Singleton(){}
//静态工厂方法
public static Singleton getInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
在不考虑并发的情况下,这种标准的可以保证取到唯一的实例。
首先,静态实例,带有static关键字的属性在每一个类中都是唯一的;
其次,私有的构造方法,可以限制客户端随意创造实例;
再次,公共的静态工厂方法,返回唯一的实例;
最后,只有在没有的时候才会实例化,有就会返回。
2.线程安全的懒汉式单例
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
这种方法是对获取实例的方法,进行加锁,也就是将整个实例获取的方法同步,这样当多个线程用时访问这个方法的时候,只有一个线程获取锁,其他线程都会处于挂起等待的状态,好处是可以保证同步访问的时候创造多个实例的现象,但是由于加锁,会是很多同步线程等待,造成了资源的浪费。
3.双重验证的单例(DCL,即 double-checked locking)
public class SingletonSync {
private static SingletonSync instance;
private SingletonSync(){}
public static synchronized SingletonSync getInstance(){
if (instance == null ){
synchronized (SingletonSync.class){
if (instance == null){
instance = new SingletonSync();
}
}
}
return instance;
}
}
假设我们去掉同步块中的是否为null的判断,有这样一种情况,假设1线程和2线程都在同步块外面判断了SingletonSync 为null,结果1线程首先获得了线程锁,进入了同步块,然后1线程会创造一个实例,此时SingletonSync 已经被赋予了实例,1线程退出同步块,直接返回了第一个创造的实例,此时2线程获得线程锁,也进入同步块,此时1线程其实已经创造好了实例,2线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,直接就是一条创建实例的语句,所以2线程也会创造一个实例返回,此时就造成创造了多个实例的情况。
经过刚才的分析,貌似上述双重加锁的示例看起来是没有问题了,但如果再进一步深入考虑的话,其实仍然是有问题的。 如果我们深入到JVM中去探索上面这段代码,它就有可能(注意,只是有可能)是有问题的。 因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。 首先要明白在JVM创建新的对象时,主要要经过三步。
1.分配内存
2.初始化构造器
3.将对象指向分配的内存的地址
这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了。
因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给SingletonSync ,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为SingletonSync 对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了SingletonSync ,就会产生莫名的错误。所以我们在语言级别无法完全避免错误的发生,我们只有将该任务交给JVM,所以有一种比较标准的单例模式。如下所示。
4.登记式(静态内部类)单例
public class Singleton {
private Singleton(){}
public static final Singleton getInstance(){
return SingletonInstance.INSTANCE;
}
private static class SingletonInstance {
static Singleton INSTANCE = new Singleton();
}
}
首先来说一下,这种方式为何会避免了上面莫名的错误,主要是因为一个类的静态属性只会在第一次加载类时初始化,这是JVM帮我们保证的,所以我们无需担心并发访问的问题。所以在初始化进行一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以singleton仍然是单例的。
5.饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
它基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果
6.枚举式
public enum Singleton {
INSTANCE;
public void uselessMethod(){
}
}
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。不能通过 reflection attack 来调用私有构造方法。
总结:
一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 5种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 3种双检锁方式。