单例的实现方法总结
以下的内容不涉及基础,比如什么是单例?JVM类加载顺序?等等。
仅仅是对所有单例的实现方法进行汇总。
一、最经典的饿汉模式实现方式
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
另外一个变种的实现方法,是将静态成员改为静态代码块
public class Singleton1_2 {
private static Singleton1_2 instance;
static {
instance = new Singleton1_2();
}
private Singleton1_2 (){}
public static Singleton1_2 getInstance() {
return instance;
}
}
不管怎么写,本质上利用的都是“类的初始化过程(包含静态成员赋值,以及静态代码块的执行),只在类被加载到内存时执行一次”这一特性。
-
优点
- 由于其原理,天然就是线程安全的
- 结构简单理解容易
-
缺点
- 相对于懒汉模式,饿汉模式最麻烦的地方在于,如果创建单例类的对象要依赖参数或者外部配置文件的话,也就是说,业务场景需要在调用getInstance方法时传入参数,决定用何种方式创建单例实例的话,饿汉模式就无法使用了。
二、懒汉模式实现方法
public class Singleton2 {
private static Singleton2 instance;
private Singleton2 (){
}
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
-
优点
- 在必须使用延迟加载的场景下,替代饿汉模式
-
缺点
- 最重要的一点,为了确保线程安全,必须使用synchronized关键字进行同步,影响性能。
三、双重检查方法
双重检查其实就是对于懒汉模式的一种性能改进,减小了synchronized关键字锁定的代码块范围。
第二重检查的作用是:防止有别的线程,在第一重检查和拿锁之间创建了单例实例。
public class Singleton3 {
private volatile static Singleton3 instance;
private Singleton3 (){
}
public static Singleton3 getSingleton() {
if (instance == null) {
synchronized (Singleton3.class) {
if (instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
四、静态内部类方法
这种方法,也是利用了类加载的特性,在getInstance()方法调用静态内部类的静态成员变量时,静态内部类SingletonHolder才会被初始化,创建单例实例。
(复习:使用 Class.staticMember 方式引用类的静态成员变量,属于对类进行主动引用,在这种情况下会触发类加载的初始化过程)
public class Singleton4 {
private static class SingletonHolder {
private static final Singleton4 INSTANCE = new Singleton4();
}
private Singleton4 (){
}
public static Singleton4 getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点
- 延迟加载
- 无锁,没有性能损耗
- 天然线程安全
五、枚举类
这是一种最简洁但是最难理解的单例实现方法。但是《Effective Java》评价这是实现单例的最佳方法(参看该书第3条)
public enum Singleton5 {
/**
* 枚举实现单例
*/
INSTANCE;
public void businessMethod() {
}
}
其调用方法如下:
Singleton5.INSTANCE.businessMethod()
下面解释枚举类为什么能实现单例。
-
首先对于Singleton5编译好的class进行反编译
因为enum只是一个关键字,不是超类或者其他能看到源码的东西。因此利用反编译的手段来确认内部实现(可以使用jad等工具)。
package singleton;
public final class Singleton5 extends Enum
{
public static Singleton5[] values()
{
return (Singleton5[])$VALUES.clone();
}
public static Singleton5 valueOf(String name)
{
return (Singleton5)Enum.valueOf(singleton/Singleton5, name);
}
private Singleton5(String s, int i)
{
super(s, i);
}
public void businessMethod()
{
}
public static final Singleton5 INSTANCE;
private static final Singleton5 $VALUES[];
static
{
INSTANCE = new Singleton5("INSTANCE", 0);
$VALUES = (new Singleton5[] {
INSTANCE
});
}
}
-
枚举如何保证线程安全
可以看到,使用enum关键字的话,实际会生成一个继承了Enum,并且final的类。
public final class Singleton5 extends Enum
注意下面的这一段:
public static final Singleton5 INSTANCE;
private static final Singleton5 $VALUES[];
static
{
INSTANCE = new Singleton5("INSTANCE", 0);
$VALUES = (new Singleton5[] {
INSTANCE
});
}
- 根据类加载过程,在“链接”的“准备”阶段,静态且final的静态成员INSTANCE被加载到了方法区(如果这里有赋值操作的话就有值了,不会等到初始化阶段,这是final与其他不同的地方)。
- 到了初始化阶段,会执行静态代码块内的内容,开辟内存空间存放单例实例,并将地址赋值给INSTANCE。
- 类加载过程是只会执行一次的,所以本质上还是利用jvm规定的类加载过程,形成了天然的线程安全
- 另外,构造函数 new Singleton5("INSTANCE", 0) 实际上调用是 super,也就是 Enum 类的构造函数,第一个参数是枚举名称(name),第二个参数是顺序(ordinal)。
-
解决反序列化问题
- 前面除了枚举类以外的单例的实现方法,都有一个弱点,如果需要进行序列化的话(implements Serializable),那么在反序列化的时候,每次调用readObject()方法都会生成一个不同于原单例的新实例,单例失效。
- 对于枚举,为了保证枚举类型符合Java相关规范(JSR),每一个枚举类及其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java有特殊处理
- 序列化的时候,Java仅将枚举类的name属性输出到结果中,反序列化的时候通过Enum的valueOf方法来根据名字查找枚举对象。
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
- 而valueOf()调用enumConstantDirectory(),继而调用getEnumConstantsShared(),可以看到里面实际调用的是反编译出来的那段代码里的value()方法,使用的实际上就是那个$VALUES[]。
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
T[] getEnumConstantsShared() {
if (enumConstants == null) {
if (!isEnum()) return null;
try {
final Method values = getMethod("values");
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
values.setAccessible(true);
return null;
}
});
@SuppressWarnings("unchecked")
T[] temporaryConstants = (T[])values.invoke(null);
enumConstants = temporaryConstants;
}
// These can happen when users concoct enum-like classes
// that don't comply with the enum spec.
catch (InvocationTargetException | NoSuchMethodException |
IllegalAccessException ex) { return null; }
}
return enumConstants;
}
- 所以枚举即使被反序列化也不会创建对象。
- 所以枚举即使被反序列化也不会创建对象。
- 所以枚举即使被反序列化也不会创建对象。
六、应对多个类加载器
前面的所有方法都有一个共通的问题:被多个类加载器加载。
这问题不算是钻牛角尖,一些热启动机制的框架,就是利用多个类加载器实现的,这时候确实有可能造成单例变成多例。
在网上找到了一段代码来解决这个问题,就是增加下面这个私有静态类。
原理是在被调用getClass方法时,直接利用自身原来的类加载器进行类加载,确保自始至终一直是同一个类加载器在加载单例类。
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}
网友评论