单例模式
单例模式是一种软件工程设计解决方案,适用于应用程序希望所有情况下当前进程中都只有一个类的实例化对象。在Java编程应用中单例模式应用非常的广泛,而且能够解决Java应用程序很多的安全问题。
我们分别讨论一下单例模式在Java应用中的几种经典实现方式。
饿汉模式
单例模式实现方案的一种,单例的实例化在单例对象实际需要之前已经被初始化于进程环境中,在Java应用中在JVM的类加载机制加载类字节流链接与初始化阶段便实例化完成。
package com.iblog.pattern.singleton;
public class UrgentSingleton {
private static UrgentSingleton INSTANCE = new UrgentSingleton();
/** private constructor */
private UrgentSingleton() {}
public static UrgentSingleton get() {
return INSTANCE;
}
}
饿汉模式单例实例实在应用初始化的时候便实例化单例,无论此单例对象是否被使用都会被初始化,如果单例对象很小,这是一种理想的实现方案,我们可能接受即使不被使用也存在于JVM中;但是如果对象很大,那么这将是一种不太理想的方案,因为应用初始化时便实例化一个大对象于JVM环境中。
懒汉模式
在编程中我们习惯性在需要使用时才会去创建对象实例,那么单例的实现其实也是可以的,我们将单例的实例化发生在我们第一次需要使用这个单例的时候,即懒汉模式。
package com.iblog.pattern.singleton;
public class LazySingleton {
private static volatile LazySingleton INSTANCE = null;
private LazySingleton() {}
public static LazySingleton get() {
if (INSTANCE == null) {
synchronized (LazySingleton.class) {
if (INSTANCE == null) { // double check.
INSTANCE = new LazySingleton();
}
}
}
return INSTANCE;
}
}
上面这个实现方案是当程序第一次调用获取单例方法时检查单例对象是否为null,如果为null尝试获取类锁并实例化此单例对象,最后返回实例化对象。
懒汉模式需要注意的是,如果当前有多个线程同时进入get方法,判断单例对象为null同时成立,那么这三个线程均会进入单例初始化逻辑,即获取类锁并初始化单例实例,所以在此处需要进行双重验证,否则会出现多个单例实例对象的情况发生。
静态代码块实现单例
我们也可以利用JVM的类加载机制来用静态代码块实现单例模式,单例的实例化放在静态代码块中实现;类的静态代码块只会被类调用执行一次,发生在类初始化时,可以确保对象只会被实例化一次。
package com.iblog.pattern.singleton;
public class StaticBlockSingleton {
private static final StaticBlockSingleton INSTANCE;
static {
try {
INSTANCE = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("StaticBlockSingleton init exception:", e);
}
}
private StaticBlockSingleton() {}
public static StaticBlockSingleton get() {
return INSTANCE;
}
}
静态内部类实现单例
Java静态内部类初始化发生在类被调用的时候,可以将单例实例化放入内部静态类中,也能保证单例的唯一存在。
package com.iblog.pattern.singleton;
public class StaticClassSingleton {
private static class LazyHolder {
private static final StaticClassSingleton INSTANCE = new StaticClassSingleton();
}
private StaticClassSingleton() {}
public static StaticClassSingleton get() {
return LazyHolder.INSTANCE;
}
}
采用静态内部类封装单例的静态成员,可以实现部分属性懒加载;在我们调用get获取实例之前LazyHolder不会被初始化,也是被广泛推荐的一种单例实现方案。
枚举实现单例
Java中的枚举特性保证了实例的单一性并且提供线程安全的隐式支持,将单例对象使用枚举封装也可以保证单例唯一存在。
public enum EnumSingleton {
INSTANCE;
public void someMethod(String param) {
// some class member
}
}
枚举的实现方式在开源代码中也经常能见到。
readResolve()实现单例反序列化
分布式应用从同一个持久化源中读取并反序列化一个单例对象,正常情况下对象反序列化会创建一个新的对象。
package com.iblog.pattern.singleton;
import java.io.Serializable;
public class Demo2Singleton implements Serializable {
private volatile static Demo2Singleton instance = null;
public static Demo2Singleton get() {
if (instance == null) {
instance = new Demo2Singleton();
}
return instance;
}
private int value = 10;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
以下测试用例会通过:
package com.iblog.pattern.singleton;
import org.junit.Test;
import java.io.*;
import static org.junit.Assert.*;
public class Demo2SingletonTest {
private Demo2Singleton instanceOne = Demo2Singleton.get();
@Test
public void testReadSolve() throws Exception {
// Serialize to a file
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
instanceOne.setValue(20);
// Serialize to a file
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
Demo2Singleton instanceTwo = (Demo2Singleton) in.readObject();
in.close();
assertNotEquals(instanceOne.toString(), instanceTwo.toString());
assertEquals(20, instanceOne.getValue());
assertEquals(10, instanceTwo.getValue());
}
}
我们从测试用例代码可以看到,反序列化对象与原单例已经不是一个对象,而是一个新的对象。我们需要保证单例,可以在单例实现类中添加readResolve()方法来确保单例对象的唯一性。
package com.iblog.pattern.singleton;
import java.io.Serializable;
public class DemoSingleton implements Serializable {
private volatile static DemoSingleton instance = null;
public static DemoSingleton get() {
if (instance == null) {
instance = new DemoSingleton();
}
return instance;
}
protected Object readResolve() {
return instance;
}
private int value = 10;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
为单例添加测试用例:
package com.iblog.pattern.singleton;
import org.junit.Test;
import java.io.*;
import static org.junit.Assert.*;
public class DemoSingletonTest {
private DemoSingleton instanceOne = DemoSingleton.get();
@Test
public void testReadSolve() throws Exception {
// Serialize to a file
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
instanceOne.setValue(20);
// Serialize to a file
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
DemoSingleton instanceTwo = (DemoSingleton) in.readObject();
in.close();
assertEquals(instanceOne.toString(), instanceTwo.toString());
assertEquals(instanceOne.getValue(), instanceTwo.getValue());
}
}
测试用例会顺利通过,表明反序列化对象与原单例对象是同一个对象。
serialVersionUId实现反序列化
反序列化目标类如果发生结构变化,JVM会抛出异常:
java.io.InvalidClassException: singleton.DemoSingleton; local class incompatible: stream classdesc serialVersionUID = 5026910492258526905, local class serialVersionUID = 3597984220566440782
at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
at java.io.ObjectInputStream.readClassDesc(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at singleton.SerializationTest.main(SerializationTest.java:24)
private static final long serialVersionUID = 1L;
Summary
单例模式最佳推荐方案推荐使用静态内部类将单例封装。
package com.iblog.pattern.singleton;
import java.io.Serializable;
public class StaticClassSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static class LazyHolder {
private static final StaticClassSingleton INSTANCE = new StaticClassSingleton();
}
private StaticClassSingleton() {}
public static StaticClassSingleton get() {
return LazyHolder.INSTANCE;
}
}
github:pattern-example