单例模式也是创建型模式的一种,在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例,而且自行实例化并且向整个系统提供这个实例。该类构造方法为私有,并 =提供一个全局访问点,比如公开一个getInstance()方法获取单个实例。单例模式在现实生活中应用也非常广泛。例如:ServletContext、 ServletContextConfig ;在 Spring 框架应用中的 ApplicationContext;数据库的连接池等。一般来说单例的创建有饿汉式,懒汉式、注册式(枚举方式)三种方式。
“饿汉模式”就是我们所说的立即加载,饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。Spring 中 IOC容器 ApplicationContext 本身就是典型的饿汉式单例。如下为饿汉式创建单例的两种方式。
public class StarvingSingle {
private static final StarvingSingle single = new StarvingSingle();
private StarvingSingle() {
}
public static StarvingSingle getInstance() {
return single;
}
}
public class StaticBlockSingle {
private static final StaticBlockSingle staticBlockSingle;
private StaticBlockSingle() {
}
static {
staticBlockSingle = new StaticBlockSingle();
}
public static StaticBlockSingle getInstance() {
return staticBlockSingle;
}
}
上面使用饿汉式的两种方式创建了一个单例模式,没有加任何锁、执行效率比较高,用户体验比懒汉式单例模式更好,但是类加载的时候就初始化,不管用与不用都占着空间,在单例特别多的时候浪费了内存。而懒汉模式则解决了这个问题。
懒汉模式就是我们所说的延迟加载,懒汉式单例的特点是:被外部类调用的时候内部类才会加载,即在我们获取Instance实例的时候才会被实例化,如下为一个简单的单例案例:
public class UnsafeLazySingle {
private static UnsafeLazySingle unsafeLazySingle;
private UnsafeLazySingle() {
}
public static UnsafeLazySingle getInstance() {
if (unsafeLazySingle == null) {
unsafeLazySingle = new UnsafeLazySingle();
}
return unsafeLazySingle;
}
}
上面的例子只有在获取实例的时候才被实例化,我们为该类起了一个UnsafeLazySingle,不安全的懒加载单例,因为再多个线程下,使用这种形式可能会产生多个实例。该方式存在线程安全隐患。那么,我们如何来优化代码,使得懒汉式单例模式在线程环境下安全呢?我们可以给getInstance()加上synchronized关键字,使这个方法变成线程同步方法:
public class SynchronizedSingle {
private static SynchronizedSingle single = null;
private SynchronizedSingle() {
}
public synchronized static SynchronizedSingle getInstance() {
if (single == null) {
single = new SynchronizedSingle();
}
return single;
}
}
使用上面的方式,当我们将其中一个线程执行并调用 getInstance()方法时,另一 个线程在调用 getInstance()方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻 塞。直到第一个线程执行完,第二个线程才恢复 RUNNING 状态继续调用 getInstance() 方法。使用这种方法线程安全的问题是解决了。但是,用 synchronized 加锁,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批 量线程出现阻塞,从而导致程序运行性能大幅下降。所以我们需要一种更好的方式,既兼顾线程安全又提升程序性能。这就是双重检查锁的单例模式:
public class SynchronizedDoubleCheckSingle {
private static SynchronizedDoubleCheckSingle single = null;
private SynchronizedDoubleCheckSingle() {
}
public static SynchronizedDoubleCheckSingle getInstance() {
if (single == null) {
synchronized (SynchronizedDoubleCheckSingle.class) {
if (single == null) {
single = new SynchronizedDoubleCheckSingle();
}
}
}
return single;
}
}
上面的两种懒加载模式,无论是不是使用双重锁检测都用到了synchronized关键字,总归是要上锁,对程序性能还是存在一定影响的。我们需要考虑一种不上锁的懒加载方式,从类初始化角度来考虑,可以采用静态内部类的方式:
public class InnerStaticSingle {
private InnerStaticSingle() {
}
private static class InnerStatic {
private static final InnerStaticSingle single = new InnerStaticSingle();
}
public static final InnerStaticSingle getInstance() {
return InnerStatic.single;
}
}
上面内部类方式创建单例兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题,又巧妙地避免了线程安全问题。上面我们使用多种方式创建了单例模式,并且解决了多线程下的安全问题,保证多线程时只有一个实例。但是其实这种创建方式还会有其它问题:
上面的例子我们实现了单例模式,但是我们还可以使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会 两个不同的实例。这就破坏了单例模式,代码如下所示:
public class ReflectSingle {
public static void main(String[] args) {
try {
InnerStaticSingle innerStaticSingle = InnerStaticSingle.getInstance();
Constructor constructor = InnerStaticSingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
InnerStaticSingle innerStaticSingle1 = constructor.newInstance();
//输出false
System.out.println(innerStaticSingle == innerStaticSingle1);
} catch (Exception e) {
}
}
}
上面的代码最终输出的是false,很显然,创建了两个不同的实例。要解决这个问题,我们只需要在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。代码如下所示,如果要创建的实例已经存在,则构造方法中直接抛出异常。
private InnerStaticSingle() {
if (InnerStatic.single != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
除了通过反射破坏单例外,通过序列化也是可以破坏单例的。当我们将一个单例对象创建好,将对象序列化然后写入到磁盘,下次使用时 再从磁盘中读取到对象,反序列化转化为内存对象时,反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当 于破坏了单例。测试代码如下:
public class SeriableStaticBlockSingle {
public static void main(String[] args) {
try {
StaticBlockSingle sbs = StaticBlockSingle.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
oos.writeObject(sbs);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
StaticBlockSingle sbs2 = (StaticBlockSingle) ois.readObject();
//输入false
System.out.println(sbs == sbs2);
ois.close();
} catch (Exception e) {
}
}
}
运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,那么,我们要保证序列化的情况下也能够实现单例,只需要增加 readResolve()方法即可。使用下面的代码,序列化之后反序列化的对象与获取的实例是一致的,上面的测试代码会输出true。至于原因可以查看JDK的ObjectInputStream类的readObject()方法的源码。
public class StaticBlockSingle implements Serializable {
private static final long serialVersionUID = 6452924321256593732L;
private static final StaticBlockSingle staticBlockSingle;
private StaticBlockSingle() {
}
static {
staticBlockSingle = new StaticBlockSingle();
}
public static StaticBlockSingle getInstance() {
return staticBlockSingle;
}
public Object readResolve() {
return staticBlockSingle;
}
}
用enum具有自由序列化,线程安全,保证单例的特性,也就是说,如果我们使用枚举创建单例模式,不用提供readResolve()方法即可保证序列化的对象与创建的对象一致。这也是Effect Java中推荐的创建单例模式的写法:如下就是最简单的枚举。
public enum EnumSingle {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingle getInstance() {
return INSTANCE;
}
}
上面我们介绍了单例的几种实现方式与一些问题,单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。至于采用哪种方式创建单例,则需要根据项目的具体要求来实现。