- 一文带你搞懂Java动态代理
- 几分钟带你搞懂策略模式
- 几分钟带你搞懂观察者模式
- 一文彻底搞明白工厂和抽象工厂
- 一文搞明白装饰者模式
- 最全单例模式
- 几段代码搞明白命令模式
- 几段代码搞明白适配器模式
- 一看就懂的外观模式
- 一看就懂的模版方法模式
- 几段代码搞懂迭代器模式
- 一文搞懂明白状态模式
单例模式
关于单例模式的定义,我就直接引用Head First 了:单例模式确保一个类只有一个实例,并提供一个全局访问点。 单例模式,按加载时机可以分为:饿汉方式和懒汉方式。我们具体看下:
懒汉加载
这是最简单的懒汉方式,但是却不能保证线程安全。所以如果项目中涉及到多线程,应避免使用。懒汉式是延迟加载的,优点在于资源利用率高,但第一次调用时的初始化工作会导致性能延迟,以后每次获取实例时也都要先判断实例是否被初始化,造成些许效率损失。
public class Singleton {
private static Singleton singleton;
/**
* 私有构造,只通过getInstance 对外提供实例
*/
private Singleton() { }
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
如果我们为上面代码加上synchronized 修饰符呢?通过添加synchronized修饰符,确实能解决线程不安全问题,但这又会引入另外一个问题,那就是执行效率问题。每个线程每次执行 getInstance() 方法获取类的实例时,都会进行同步,而事实上实例创建完成后,同步就变为不必要的开销了。
public class Singleton {
private static Singleton singleton;
private Singleton() { }
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
通过上面我们发现,要想保持同步,还要兼顾效率问题,那么一个自然而然的思路就是:缩小同步区域的范围了。这样同步块的方式就出来了。
public class Singleton {
private static Singleton singleton;
private Singleton() { }
/**
* 这样存在线程问题。
*
* <p>可能存在多个线程同时通过第一次检查,
* 导致创建了不同的对象。
*/
public static Singleton getInstance() {
// 第一次检查,避免了不必要的同步
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
针对上面的情况,我们增加第二次检查,来保证多线程问题。代码见下:
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if(Singleton == null){ // 第一次检查,避免不必要同步
synchronized (Singleton.class) {
if(Singleton == null){ // 第二次检查,线程安全
singleton = new Singleton();
}
}
}
return singleton;
}
}
到这里就高枕无忧了吗?并不是,双重检查加锁(DCL)会由于 Java 编译器允许处理器乱序执行,所以会有 DCL 失效的问题。好在在JDK1.5 版本后,Java提供了volatile 关键字,来保证执行的顺序,从而使单例起效。至于在JDK1.5版本以下,我们应避免使用该方式。双重检查加锁 单例代码见下:
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if(Singleton == null){
synchronized (Singleton.class) {
if(Singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
volatile关键字,是因为volatile确保了应用中的可视性,如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改。即便使用了本地缓存,情况也是如此,volatile域会立即被写入到主存中,而读取操作就发生在主存中(摘自java编程思想)。其实就是为了防止编译器优化指令执行,防止指令重排序问题,让每次操作都是到主存中对同一份数据进行读写,防止发生问题。
饿汉加载
饿汉式天生是线程安全的。在只考虑一个类加载器的情况下,饿汉方式在系统运行起来装载类的时候就进行初始化实例的操作,由 JVM 虚拟机来保证一个类的初始化方法在多线程环境中被正确加锁和同步。饿汉式在类创建的同时就实例化了静态对象,其资源已经初始化完成,所以第一次调用时更快,优势在于速度和反应时间,但是不管此单例会不会被使用,在程序运行期间会一直占据着一定的内存。
public class Singleton {
public static final Singleton instance = new Singleton();
private Singleton() { }
}
静态代码块的饿汉式加载同样能保证线程安全。
public class Singleton {
private static final Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton () { }
public static Singleton getInstance() {
return instance;
}
}
这是我们常用的书写形式,与上面两种一样。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
静态内部类式加载
当未使用InterSingleton 静态内部类时,InterSingleton 类并不会被初始化,只有在显式调用 getInstance() 方法时,才会装载 InterSingleton 类,从而实例化对象,所以能达到延迟实例化的目的。同时在静态初始化器中创建单件,也保证了线程的安全。此方式也避免了在JDK 版本低于 1.5 时双重检查加锁方式的缺陷。
public class Singleton {
private Singleton(){}
public static Singleton getInstance() {
return InterSingleton.singleton;
}
private static class InterSingleton {
private static Singleton singleton = new Singleton();
}
}
枚举方式
枚举不仅在创建实例的时候默认是线程安全的,而且在反序列化、反射、克隆时都可以自动防止重新创建新的对象。枚举类也是在第一次访问时才被实例化,属于懒加载方式。由于枚举是 JDK 1.5 才加入的特性,所以双重加锁检查的方式一样,对于版本有一定要求。
/**
* 枚举同Java中的普通Class一样,
* 也可以添加自己的属性和方法。
*/
public enum Singleton {
INSTANCE;
}
Map登记式单例
随着项目越来越复杂,我们可能会需要同时管理多个不同业务的单例。这时我们就可以通过Map容器来统一管理这些单例,使用时,通过统一的接口来获取相应单例。
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String,Object>();
private SingletonManager() { }
private static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}