单例模式
饿汉式
在类加载的时候直接将Singleton对象提前创建好,等需要用的时候调用getSingleton()
方法获取即可。
提前创建,是一种空间换时间的方式,无法实现延迟加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
class Singleton { private Singleton() { }
private static final Singleton s = new Singleton();
public static Singleton getS() { return s; } }
|
JDK源码中的典型实现: java.lang.Runtime
类 和 Unsafe
类(只不过获取Unsafe实例时往往使用反射打破单例模式强行创建)。
JVM中的Runtime类就是饿汉式单例模型的典型实现,只能通过 Runtime.getRuntime()
方法获取其实例对象。
懒汉式
等需要使用单例对象的时候才去实例化对象,是一种时间换空间的延迟加载方式,线程不安全,实际开发中一般不用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
class Singleton { private Singleton() { }
private static Singleton s;
public static Singleton getS() { if (s == null) { s = new Singleton(); } return s; } }
|
基于synchronized 线程安全型懒汉式
对于上述线程安全的问题,可以使用synchronized关键字加锁来保证线程安全,不过,加锁会导致串行化,会在一定程度上影响代码性能。
不过随着JDK对synchroniezd锁的优化,这种性能损耗也是越来越小,可以忽略不计。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Singleton { private Singleton() { }
private static Singleton s;
public static synchronized Singleton getSingleton() { if (s == null) { s = new Singleton(); } return s; } }
|
DCL(Double Check Lock)
DCL(Double Check Lock),双重检查锁,比线程安全型的懒汉式运行效率更高。
对获取单例对象的请求有分流,只在获取不到单例对象的时候才会去创建对象;在单例对象存在时直接获取,方法的执行效率高;
DCL存在的问题:
由于JVM在编译和运行的时候都会对代码进行一定的优化,比如:指令重排;因此可能会导致NullPointerException – Singleton对象创建过程有指定重排。
当一个线程执行请求获取单例对象,进入同步代码块创建一个单例对象;另外一个线程也尝试获取单例对象,判断得知单例对象已经创建,直接返回创建的对象供其使用,但是在使用的过程中可能会出现空指针异常,因为指令重排可能会导致对象是创建出来了,但尚未完成初始化,所以另外一个线程获取的对象内部可能有值为null的属性;
JVM中对象的实例化过程简单地说:加载、链接、初始化、使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class Singleton {
private static Singleton instance;
private Singleton() { }
public static Singleton getInstance() { if (null == instance) { synchronized (Singleton.class) { if (null == instance) { instance = new Singleton(); } } } return instance; } }
|
禁止指令重排的DCL
volatile 关键字会禁止指令重排,保证创建对象的过程是有序的,即可保证使用的对象都是已经完成了初始化操作的完整对象;
JVM使用指令重排的目的就是优化性能,所以频繁使用volatile禁止指令重排会影响性能;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class Singleton { private static volatile Singleton instance;
private Singleton() { }
public static Singleton getInstance() { if (null == instance) { synchronized (Singleton.class) { if (null == instance) { instance = new Singleton(); } } }
return instance; } }
|
Holder
懒加载|线程安全|效率高,充分利用了JVM的类加载机制。
JVM中的类加载机制为懒加载,一个应用在启动的时候不会一上来就直接加载应用中所有引入的类,而是加载应用启动时有使用的类,剩余没有加载的类只有在用到的使用才会加载。所以从Java应用启动的时候,就会创建一个方法区,随着业务场景的调用,使用到的类越来越多,方法区中的已使用内存也会越来越大,直到该应用中所有用到的类都经历了加载,那么方法区中的已使用内存大小就会趋于稳定不变。
当然了,也有例外,方法区中存放的是类信息和字符串常量池(JDK1.6),针对这两块在程序运行时还有有可能会越来越大。
类信息:运行时不断地增加加载的类。
- 自定义类加载器,去自定目录下加载自定义类;
- 反射,通过反射不断地生成Class文件;
字符串常量池:不断地创建字符串对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public class Singleton {
private Singleton() { }
private static class InstanceHolder { public static final Singleton INSTANCE = new Singleton(); }
public static Singleton getInstance() { return InstanceHolder.INSTANCE; } }
|
枚举
线程安全|懒加载|高效|优雅
充分利用枚举项的构造器只会执行一次的特性来创建单例对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class Singleton {
private Singleton() { }
private enum SingletionEnum { INSTANCE;
private final Singleton singleton; SingletionEnum() { this.singleton = new Singleton(); } public Singleton getSingleton() { return singleton; } }
public Singleton getInstance() { return SingletionEnum.INSTANCE.getSingleton(); } }
|
打破单例
Singleton单例模式从其定义出发就是永远获取的是同一个对象,但是可以通过反射来打破单例;
单例实现的核心就是通过私有化构造器,让外界无法通过构造器来创建对象,而是在类的内部维护了一个对象的引用,来实现单例;而反射可以在系统运行时通过Class对象来获取对象的构造器,包括私有构造器,然后通过获取的构造器对象来创建对象;这样就打破了单例模式。
Demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
class SingletonFactory { private static Singleton singleton;
static { try { Class<?> aClass = Class.forName(Singleton.class.getName()); Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(); declaredConstructor.setAccessible(true); singleton = (Singleton) declaredConstructor.newInstance(); } catch (Exception e) { e.printStackTrace(); } }
public static Singleton create() { return singleton; } }
|
上述代码获取的就是一个新的单例对象,和Singleton.getIntance()
获取的不是同一个对象;但是利用上述方法获取的都是同一个对象;因为创建对象的过程是在静态代码块中,所以创建对象的过程只会在类加载的时候执行一次,所以通过create()
方法获取的都是同一个对象;
也可以写的更过分,将单例粉碎了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class SingletonFactory { public static Singleton create() { Singleton singleton = null; try { Class<?> aClass = Class.forName(Singleton.class.getName()); Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(); declaredConstructor.setAccessible(true); singleton = (Singleton) declaredConstructor.newInstance(); } catch (Exception e) { e.printStackTrace(); } return singleton; } }
|
上述代码每获取一次对象,都是通过反射重新创建一个新的对象,返回的结果没有一个是相同的,都是新生成的对象,单例已经不复存在了。