单例模式
饿汉式、DCL懒汉式
饿汉式:
java
//饿汉式单例
public class Hungry {
private Hungry() {
}
//类加载的时候就会创建出实例对象,可能造成不必要的内存浪费
private final static Hungry hungry = new Hungry();
public static Hungry getInstance() {
return hungry;
}
}
DCL懒汉式:
java
package GoF设计模式.single;
//懒汉式单例
public class Lazy {
private Lazy() {
}
private static volatile Lazy lazy;
//DCL懒汉式,只有调用方法的时候,才会去实例化对象
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
/* lazy = new Lazy();这一步操作,实际上在底层分为三步
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向者个空间
* 因此,可能出现指令重排的情况
*
* 我们希望的执行顺序是:123
* 但如果出现指令重排:132
*
* 就会出现线程A执行先执行1、3,此时单例对象lazy还未被初始化
*
* 这时候线程B进来了,判断lazy不为空,就会返回一个未初始化的对象! //此时lazyMan还没有完成构造
* 因此,要加上volatile关键字防止指令重排
* */
}
}
}
return lazy;
}
}
枚举类实现:
java
//枚举类实现单例
public enum Singleton {
//枚举类是天生的单例对象,这个Singleton对象INSTANCE,天生就是单例的。不需要考虑什么线程问题、反射问题...
INSTANCE;
public void doSomething(){
System.out.println("单例对象调用该方法...");
}
}
java
public static void main(String[] args) throws Exception {
//饿汉式和懒汉式没必要说明怎么使用了,直接调用getInstance方法就可以获取实例
//稍微说明一下枚举类
//正常枚举类单例模式使用
Singleton instance = Singleton.INSTANCE;
instance.doSomething();
//如果在多线程的情况下也是不会有问题的
for (int i = 0; i < 10;i++){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Singleton.INSTANCE.hashCode());
}
}).start();
}
//尝试反射创建两个不同的INSTANCE
Constructor<Singleton> declaredConstructor
= Singleton.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
Singleton instance2 = declaredConstructor.newInstance();
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
从打印结果可以看到,JVM会抛出异常,不能通过反射创建一个枚举类对象。
总结
(1)单例模式常见的写法有两种:懒汉式、饿汉式
(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题
(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。
(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;
(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题
(6)为了防止多线程环境下,因为指令重排序导致变量报NPE,需要在单例对象上添加volatile关键字防止指令重排序
(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。