单例模式的应用
单例是所有设计模式里面最简单但是应用最广的一种,它的作用是维护一个全局唯一的对象。
单例可以节约JVM的内存空间,不用每次都去new一个对象,单例只初始化一次就可以共享使用。
单例模式的实现
单例模式可以分为懒汉式和饿汉式:
懒汉式:在第一个获取单例对象时初始化。
饿汉式:在类加载时就完成了单例对象初始化。
懒汉式
线程不安全
public class SingleThread {
private static SingleThread singleton = null;
private SingleThread() {
}
public static SingleThread getSingleton() {
if (singleton == null) {
singleton = new SingleThread();
}
return singleton;
}
}
大家看出这并不是线程安全,如果线程A和线程B同时调用getSingleton方法,那么当线程A和线程B可能得到的是两个不同的SingleThread对象。也有可能线程A或者线程B得到一个NULL对象。
让我们来优化一下。
加锁
class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
现在已经是一个线程安全的单例类了,但是synchronized是JDK提供的一种重量级锁,消耗不小。而且每次获取单例的时候都要经过这个锁。
再从性能上考虑一下就是当下最流行的双重锁单例。
双重校验锁
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//防止指令重排
singleton = new Singleton();
}
}
}
return singleton;
}
}
这里是在单例还未初始化时才会采用synchronized锁住对请求进行排队。这里有一个关键的地方就是volatile修饰符
因为singleton = new Singleton(),这段代码其实是分为三步:
- 分配内存空间;
- 初始化对象;
- 将 singleton 对象指向分配的内存地址。
加上volatile是为了让以上的三步操作顺序执行,如果第二步在第三步之前被执行就有可能某个线程拿到的单例对象是NULL。
饿汉式
类加载
public class CommonSingleton {
private static final CommonSingleton singleton = new CommonSingleton();
private CommonSingleton() {
}
public static CommonSingleton getSingleton() {
return singleton;
}
public static void doSomething() {
}
}
这种饿汉模式是单例模式中最简单的一种实现方式,饿汉模式在类加载的时候就对实例进行创建。这种方式依赖JVM类加载机制,避免了多线程的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
静态内部类
public class MultithreadSafety {
private MultithreadSafety() {
}
public static MultithreadSafety getSingleton() {
return Inner.singleton;
}
private static class Inner {
private static MultithreadSafety singleton = new MultithreadSafety();
}
}
这应该是最优的单例实现方式,也是依靠类加载机制来保证只创建一个实例,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。
这种方式不仅确保了线程的安全性,也能够保证对象的唯一性,同时也是延迟加载!
总结
单例模式的目的就是为了保证一个全局唯一的对象。只要处理好第一次创建这个对象的时候的多线程问题,然后为了提高JVM的内存利用率,加上延迟加载的设计。就OK了。