细读源码之设计模式之单例模式

1995年,GoF(Gang of Four)四人合作出版了《设计模式:可复用面向对象软件的基础》一书,里面共收录了23种设计模式,从此树立了软件设计模式领域的里程碑,人称《GoF设计模式》。《GoF设计模式》并不是一种具体技术,而是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及对类的关联关系和组合关系运用的最佳实践。

今天我们选择设计模式中相对简单的单例模式来进行讲解,具体包含下面四个章节的内容:

一.单例模式的简介;

二.单例模式在JAVA实现中常见的5种写法;

三.单例模式评价指标:线程安全,懒加载,单例模式破坏与反破坏;

四.单例模式和工具类比较。

一.单例模式简介

单例模式(Singleton Pattern)是23种设计模式中最简单的设计模式之一,它属于创建型模式,用于返回该对象的实例。单例模式只跟本身的一个类有关系,该类负责创建自己的对象,同时确保只有唯一的一个对象被创建。

在Java中,实现单例模式都有两个必不可少的步骤:

1.将类的所有构造函数的可见性设置为private,避免该类的实例被外部代码创建;

2.提供一个公有的静态方法,一般方法名为getInstance,来返回该对象的唯一的对象实例。

下面具体看一下单例模式在JAVA实现中的5中常见写法。

二.单例模式的5种写法

1.饿汉


public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance() {
        return instance;
    }
}

饿汉模式的缺点是没有使用懒加载,可能会导致内存的浪费。

2.懒汉


public class LazySingleTon {
    private static LazySingleTon instance;
    private LazySingleTon() {
    }
    public static synchronized LazySingleTon getInstance() {
        if (instance == null) {
            instance = new LazySingleTon();
        }
        return instance;
    }
}

懒汉模式的缺点是每次获取单例对象,都需要synchronized加锁调用,性能会下降。

3.DCL(double check lock)

public class DCLSingleton {
    private static volatile DCLSingleton instance;
    private DCLSingleton() {
    }
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

DCL双重检查,需要注意的是instance字段需要加上volatile修饰。因为instance = new DCLSingleton()并不是一个原子操作,从字节码上来看,一共涉及4个操作(new,dup,invokespecial,putstatic),其中invokespecial是调用构造函数进行初始化,putstatic是讲new出来的引用值赋值给instance,这两个操作没有相互依赖关系。如果不加volatile修饰,在多线程并发访问的时候,可能导致重排序,先执行putstatic,后执行invokespecial,在这两个操作之间,如果另外一个线程执行getInstance,此时instance == null是不成立的,直接返回了instance,这样就获取到一个没有初始化的单例对象,后面可能会导致意外的结果。

4.内部类

public class InnerClassSingleton {
    private InnerClassSingleton() {
    }
    public static InnerClassSingleton getInstance() {
        return InnerClassSingletonHold.INSTANCE;
    }
    private static class InnerClassSingletonHold {
        private final static InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }
}

内部类是个人比较喜欢的单例实现方式,使用懒加载,没有使用synchronized方法,并且代码相对简洁。

5.枚举

public enum EnumSingleTon {
    INSTANCE;
    public static EnumSingleTon getInstance() {
        return INSTANCE;
    }
}

枚举单例,是《Effective Java》中推荐的单例模式,唯一就是没有使用懒加载。

三.单例模式评价指标

1.线程安全

单例的线程安全主要通过两种方式来实现:

A.用类初始化过程的线程安全来保证单例的线程安全

JVM虚拟机规范中的Initialization这一章节详细描述了类初始化的过程,以及线程安全的讲解,有兴趣的同学,可以自己研究一下文档,这里通过代码进行验证,代码如下:

public class ClassInitializationTest {
    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread A start");
                System.out.println(SingletonImpl.INSTANCE);
            }
        });
        threadA.start();
        Thread.sleep(100);
        
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread B start");
                System.out.println(SingletonImpl.INSTANCE);
            }
        });
        threadB.start();
    }

    private static class SingletonImpl {
        private static final SingletonImpl INSTANCE = new SingletonImpl();
        private SingletonImpl() {
            System.out.println("constructor start.");
            try {
                Thread.sleep(1000000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("constructor end.");
        }
    }
}

线程A先启动,打印thread A start,然后获取SingletonImpl.INSTANCE的时候,会对SingletonImpl进行类初始化,类初始化调用INSTANCE = new SingletonImpl(),然后执行SingletonImpl的构造函数,打印constructor start,然后线程A休眠。

线程B后启动,打印thread B start,在获取SingletonImpl.INSTANCE的时候,需要SingletonImpl类初始化完成,但是此时线程A已经在进行初始化了,这时候线程B只能等待。

线程A休眠了1000000毫秒后唤醒,打印constructor end。最后类初始化完成,唤醒threadB。

线程A和现场B分别打印SingletonImpl.INSTANCE。

饿汉和内部类就是使用类初始化过程的线程安全,来保证单例的线程安全。

B.使用同步锁来实现线程安全

懒汉使用synchronized同步方法来保证线程安全,而双重检查(Double Check Lock)则使用synchronized同步块加volatile来实现线程安全。

2.懒加载

懒加载和延时加载是一个意思,就是在对象必须用到的时候再去初始化,这么做的目的是为了避免已经初始化完成的对象,如果后来一次也没有被使用,白白浪费的CPU时间和内存。JDK中HashMap对table的初始化就是一种懒加载策略,构造函数执行完成后table字段还是null,只有往HashMap中put元素的时候,才对table字段进行初始化。

与懒加载相反的策略是预加载,预加载是指在服务的启动的时候,就把后面可能会用到的数据都提前都初始化了,初始化完成以后,以最好的状态对外提供服务。预加载的目的是为了解决初次访问时,由于数据初始化的额外耗时,而导致用户访问时间过长,甚至超时导致的服务不可用。

一般情况下,对内存敏感的系统,我们采用懒加载策略,用时间换空间;而对时间敏感的系统,我们采用预加载策略,用空间换时间。

3.单例模式破坏

A.通过反射调用private构造函数创建对象破坏单例

虽然单例模式中将类的构造函数设置private,但是我们还是可以通过反射的方法,去调用private的构造函数,创建出类的新对象出来,从而破坏单例唯一实例的特性,具体代码如下:

public static void main(String[] args) throws Exception {
    Class clazz = HungrySingleton.class;
    Constructor constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    HungrySingleton newSingleton = (HungrySingleton) constructor.newInstance();
    HungrySingleton realSingleTon = HungrySingleton.getInstance();
    System.out.println(newSingleton == realSingleTon);
}

最后打印false,说明newSingleton和realSingleTon是两个不同的对象。

B.不调用构造函数创建对象破坏单例

一般情况下,创建一个对象,都会调用类的构造函数进行初始化。但是有些情况,可以不调用类构造函数,就可以创建出一个对象,代码如下:

public static void main(String[] args) throws Exception {
    HungrySingleton singleton = HungrySingleton.getInstance();
    Constructor superConstructor = singleton.getClass().getSuperclass().getConstructor();
    ReflectionFactory reflectFactory = ReflectionFactory.getReflectionFactory();
    Constructor thisConstructor = reflectFactory.newConstructorForSerialization(singleton.getClass(), superConstructor);
    HungrySingleton newInstance = (HungrySingleton) thisConstructor.newInstance();
    System.out.println(newInstance == singleton);
}

最后打印false,说明newSingleton和realSingleTon是两个不同的对象。需要注意的是thisConstructor构建函数,并不是HungrySingleton的private构造函数。这块代码原理的解析,到时候单独写一篇文章进行讲解,本文还是主要讲单例模式。

C.通过Serializable进行单例破坏

我们先把单例对象进行序列化,然后进行反序列化,就会得到一个新的对象,前提的序列化的类实现了Serializable接口,代码如下:

public static void main(String[] args) throws Exception {
    HungrySingleton singleton = HungrySingleton.getInstance();
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(outputStream);
    oos.writeObject(singleton);
    oos.flush();

    ObjectInput ois = new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
    HungrySingleton deserializeSingleton = (HungrySingleton) ois.readObject();
    ois.close();

    System.out.println(deserializeSingleton == singleton);
}

最后打印false,说明newSingleton和realSingleTon是两个不同的对象,ObjectInputStream在readObject的时候,会使用上面的reflectFactory.newConstructorForSerialization,然后newInstance创建对象,同样不调用HungrySingleton的private构造函数。

D.通过Clone进行单例破坏

如果使用Clone进行单例破坏,单例类就必须实现Cloneable接口。既然把类设计成单例模式,再去实现Cloneable接口,就显得非常矛盾。单例模式要求只有一个实例,Clone的目的则是复制多个对象实例。单例类的设计者,不会给你这样的破坏机会。Clone破坏的实质是,在Clone对象的时候,直接进行的内存操作,并不调用构造函数,private可见效的构造函数就拦不住。

4.单例模式反破坏

A.在构造函数里面对instance进行非空验证,避免被反射调用到。对于饿汉单例模式,如果instance != null,直接抛出异常,组织对象的创建,代码如下:

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
        if (instance != null) {
            throw new RuntimeException("this is singleton class,you cannot call private constructor method");
        }
    }
    public static HungrySingleton getInstance() {
        return instance;
    }
}

B.在构造函数中对调用栈进行检查,验证调用构造函数的方法符合预期,如果不符合,就直接抛出异常。对于上面的饿汉单例,预期可以调用构造函数的方法是类的静态构造方法,验证代码如下:

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
        StackTraceElement traceElement = Thread.currentThread().getStackTrace()[2];
        if (!(HungrySingleton.class.getName().equals(traceElement.getClassName()) && "<clinit>".equalsIgnoreCase(traceElement.getMethodName()))) {
            throw new RuntimeException("this singleton class,you cannot call");
        }
    }
    public static HungrySingleton getInstance() {
        return instance;
    }
}

说明一下,调用栈第0层是getStackTrace方法,第1层是构造函数<init>方法,第2层就是调用构造函数的方法。所以取Thread.currentThread().getStackTrace()[2],然后判断HungrySingleton.class.getName().equals(traceElement.getClassName()) && "<clinit>".equalsIgnoreCase(traceElement.getMethodName())是否为true,不为真就说明构造函数被意外调用,这时抛出异常,阻止实例创建。

四.单例模式和工具类比较

单例模式和工具类最大的区别就是看是否有状态,具体而言,单例模式是有状态的,而工具类是无状态的。

举例说明:

平时最常见的单例模式应用场景:任务管理器,文件管理器,配置管理器这类应用,管理的任务,文件和配置都是有状态的,选择单例更加合适。

平时最常见的工具类Math类,则它是无状态的,提供了一系列的工具方法,如sin,cos,abs等。

最后我们通过一个表格来总结一下本文主要内容:

 线程安全懒加载单例破坏
懒汉
饿汉
DCL
内部类
枚举

我们在选择单例的实现方式时,如果不需要懒加载,最好选择枚举实现;如果要求必须懒加载,则选择内部类,但是面临单例被破坏的风险。

更多设计模式的讲解,尽在VIP课程《坦克大战》。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值