单例模式
单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
特点
单例模式有 3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
优缺点
单例模式的优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
单例模式的缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
扩展
单例模式可扩展为有限的多例(Multitcm)模式,这种模式可生成有限个实例并保存在 ArrayList 中,客户需要时可随机获取。
线程安全的单例模式
Java可以通过以下5中方式实现线程安全的单例模式。
实现1:饿汉式
类加载时会同时创建单例。
public final class Singleton1 {
private Singleton1(){}
private static final Singleton1 INSTANCE = new Singleton1();
public static Singleton1 getInstance(){
return INSTANCE;
}
}
问题:
- 为什么类声明上要加
final
?
答:防止该类在被继承时修改了构造方法或getInstance()
方法,进而导致单例逻辑被破坏。 - 如果单例类实现了序列化接口,还要做什么来防止反序列化破坏单例?
答:需要在单例类中额外实现一个方法readResolve()
,并在方法中将单例对象返回。之所以可以这么做,是因为java在反序列化时,如果发现该类存在readResolve()
方法,会将该方法的返回值作为反序列化的结果,而不会对序列化后的对象进行反序列化。
public final class Singleton1 implements Serializable {
private Singleton1(){}
private static final Singleton1 INSTANCE = new Singleton1();
public static Singleton1 getInstance(){
return INSTANCE;
}
public Object readResolve(){
return INSTANCE;
}
}
- 为什么将构造方法设置为私有的?
答:将构造方法设置为私有,可以防止其它类通过new Singleton1()
的方式创建对象 - 单例模式是否能够防止通过反射创建新的实例?
答:不能防止通过反射创建新的实例。通过反射中的getDeclaredConstructor()
方法,我们可以获得类的无参构造器,然后可以通过setAccessible()
方法修改构造器的访问权限,之后便可以通过该构造器创建实例对象,具体使用方法如下。注意:无法通过getConstructor()
获得无参构造器,因为getConstructor()
只能够获得类型为public的构造器。
class ReflectDemo{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 通过getInstance方法获得类对象的实例
Singleton1 instanceFromMethod = Singleton1.getInstance();
// 获得Singleton1的无参构造器
Constructor<Singleton1> constructor = Singleton1.class.getDeclaredConstructor();
// 将构造器变成可访问的
constructor.setAccessible(true);
// 用该构造器创建实例
Singleton1 instanceFromConstructor = constructor.newInstance();
// 比较两个实例是否相同
System.out.println("instanceFromMethod == instanceFromConstructor ? " + (instanceFromMethod == instanceFromConstructor));
}
}
- 能否保证
private static final Singleton1 INSTANCE = new Singleton1();
在创建单例对象时是线程安全的?
答:可以。在第一次调用Singleton1
时,代码private static final Singleton1 INSTANCE = new Singleton1();
会在类加载器中执行,JVM会保证类加载时的线程安全(加锁的),进而保证了创建单例对象时的线程安全。加载类的代码如下所示,通常情况下getClassLoadingLock(name)
会返回this
,即类加载器对象本身。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
// 加载类的具体操作...
return c;
}
}
- 为什么要提供静态方法而不是直接将
INSTANCE
设置为public?
答:首先,将INSTANCE
设置为public是没有问题的。但是提供静态方法能够:(1)提供了更好的封装性(2)解耦合,方便后续代码的修改和改进(3)提供泛型的支持。
实现2:使用方法级synchronized的懒汉式
public final class Singleton2 implements Serializable {
private Singleton2(){}
private static Singleton2 INSTANCE = null;
public synchronized static Singleton2 getInstance(){
if (INSTANCE != null){
return INSTANCE;
}
INSTANCE = new Singleton2();
return INSTANCE;
}
public Object readResolve(){
return getInstance();
}
}
缺点:将整个getInstance()
方法锁死,锁的开销较大
实现3:使用double-check的懒汉式
public final class Singleton3 implements Serializable {
private Singleton3(){}
private static volatile Singleton3 INSTANCE = null;
public static Singleton3 getInstance(){
if (INSTANCE != null){
return INSTANCE;
}
synchronized (Singleton3.class){
if (INSTANCE == null){
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
public Object readResolve(){
return getInstance();
}
}
问题
- 为什么要加 volatile?
答:为了防止指令重排现象的产生。在执行INSTANCE = new Singleton3();
时,通常的做法为先初始化对象,然后给静态变量赋值。但是JVM可能会对代码进行指令重排,其先后顺序变成先给静态变量赋值然后初始化对象,这种情况会造成比较严重的bug,具体案例见下图。
- 为什么
synchronized
中需要再次进行空判断?
答:由于synchronized
的存在,多个线程中仅有一个线程能够创建对象,当该线程退出时,会有其它线程抢到锁。如果后续线程不进行空判断,那么它们每个线程都会生成一个对象,不符合单例模式的要求。因此,在synchronized
中进行空判断,可以确认是否已经有其它线程已经创建了实例,如果是,这直接使用创建好的实例即可。
实现4:枚举类
枚举类实现单例时Java官方推荐的做法。
public enum Singleton4{
/**
* 单例
*/
INSTANCE;
}
它在IDEA中反编译的代码
public enum Singleton4 {
INSTANCE;
private Singleton4() {
}
}
它在JclassLib中反编译后的<clinit>方法如下
0 new #4 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4>
3 dup
4 ldc #7 <INSTANCE>
6 iconst_0
7 invokespecial #8 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4.<init>>
10 putstatic #9 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4.INSTANCE>
13 iconst_1
14 anewarray #4 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4>
17 dup
18 iconst_0
19 getstatic #9 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4.INSTANCE>
22 aastore
23 putstatic #1 <info/kuangkuang/exercise/concurrent/pattern/singleton/Singleton4.$VALUES>
26 return
问题
- 枚举单例是如何限制实例个数的?
答:从IDEA的反编译结果中可以看到,枚举类的构造器是私有的,因此不能通过构造器创建对象。另一方面,通过JclassLib反编译后的字节码可以看出,INSTANCE
其实是枚举类中实例化的一个静态成员变量。 - 枚举单例在创建时是否有并发问题?
答:没有。INSTANCE
作为静态成员变量,会在类加载时被创建,由类加载器保证其线程安全。 - 枚举单例能否被反射破坏单例?
答:不能。我们可以用类似在实现1中所用到的方反射法进行尝试,但是会发现无法通过getDeclaredConstructor()
方法获得枚举类的无参构造器。然而,我们可以使用getDeclaredConstructors()
获得该类的所有构造器,然后对它们进行遍历。但是,在执行newInstance()
方法时,代码会抛出异常IllegalArgumentException: Cannot reflectively create enum objects
,我们也无法通过发射实例化枚举类的对象,进而无法破坏单例模式。
class ReflectEnumDemo{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 通过getInstance方法获得类对象的实例
Singleton4 instanceFromEnum = Singleton4.INSTANCE;
// 获得Singleton4的所有构造器
Constructor<?>[] constructors = Singleton4.class.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
constructor.setAccessible(true);
Object instanceFromConstructor = constructor.newInstance();
System.out.println("(instanceFromConstructor == instanceFromEnum) ? " + (instanceFromConstructor == instanceFromEnum));
}
}
}
- 枚举单例是否可以被序列化和反序列化?如果可以,枚举类能否被反序列化破坏单例?
答: 枚举单例可以被序列化和反序列化,因为枚举类Enum
实现了Serializable
接口。但是反序列化无法破坏枚举类单例。这其实涉及到反序列化原理,枚举类与普通类在反序列化的实现上并不相同,具体的源码分析见本文最后一部分。普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。枚举类型在序列化的时候Java仅仅是将枚举对象的name
属性输出到结果中,反序列化的时候则是通过java.lang.Enum
的valueOf()
方法来根据名字查找枚举对象。 - 枚举单例属于懒汉式还是饿汉式?
答:单例对象作为枚举类的静态成员变量,会随着类的加载而产生,属于饿汉式。 - 枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?
答:枚举类可以重写其构造方法,以便于在创建单例时加入初始化逻辑,例如下面这段代码。
public enum Singleton4{
/**
* 单例
*/
INSTANCE("单例");
String name;
String desc;
Singleton4(String name) {
this.name = name;
this.desc = "这是一个"+name;
}
}
实现5:静态内部类
public final class Singleton5 implements Serializable {
private Singleton5(){}
private static class LazyHolder{
private static final Singleton5 INSTANCE = new Singleton5();
}
public static Singleton5 getInstance(){
return LazyHolder.INSTANCE;
}
public Object readResolve(){
return getInstance();
}
}
问题
-
此方法属于懒汉式还是饿汉式?
答:此方法属于懒汉式。因为类的加载本身时懒惰的,它只有在被用到时才会加载。在本场景中,如果只使用了Singleton5
而没有调用其getInstance()
方法,则不会调用到其中静态内部类LazyHolder
,也不会触发静态内部类的加载,因此此时LazyHolder
中的INSTANCE
还没有被实例化,直到有方法调用了Singleton5.getInstance()
。 -
在调用
getInstance()
时是否会有并发问题?
答:不会。类加载会保证创建时的线程安全。
枚举类反序列化的源码解读
反序列化的使用例
public class EnumUnserialize {
public static void main(String[] args) throws IOException, ClassNotFoundException {
TestEnum instance = TestEnum.INSTANCE;
String pathname = "instance.obj";
// 1. 序列化
// 1.1 搭建输出目标流,这里将其输出到文件中
FileOutputStream fos = new FileOutputStream(pathname);
// 1.2 搭建对象输出流
ObjectOutput objectOutput = new ObjectOutputStream(fos);
// 1.3 写入对象
objectOutput.writeObject(instance);
// 1.4 关闭流
objectOutput.close();
// 2. 反序列化
// 2.1 获取文件对象
File file = new File(pathname);
// 2.2 获取文件输入流
FileInputStream fis = new FileInputStream(file);
// 2.3 搭建对象输入流
ObjectInput objectInput = new ObjectInputStream(fis);
// 2.4 读出对象,并进行强转
TestEnum object = (TestEnum)objectInput.readObject();
// 2.5 关闭流
objectInput.close();
}
}
enum TestEnum{
INSTANCE;
}
运行该代码,没有发现异常,说明代码正常执行,观察文件目录,发现确实生成了instance.obj这一文件。
在该代码中,我们主要关注objectInput.readObject()
的具体实现过程,下面对该代码进行一个解读。
源码解读
readObject()
方法
源码中readObject()
方法并不长,我们也很容易将其进行拆解。
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
freeze();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
可以观察到,最后返回返回值来源有两处,一处位于前3行的if语句中,另一处则来源于readObject0(false)
。
我们首先对if语句进行判断,发现enableOverride
只会在初始化ObjectInputStream
对象时会被赋值。
其中,我们调用public ObjectInputStream(InputStream in)
时会赋值为false
,调用protected ObjectInputStream()
时会被赋值为true
。显然,我们无法调用后者,因此enableOverride
可以默认为false
,此时判断条件不成立,返回值只会来源于readObject0(false)
。
readObject0()
方法
源码中readObject0()
方法比较长,但是不难发现,其中大部分的返回来源于switch语句,其返回策略依赖于变量tc
。
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
通过代码变量的含义,不难猜到普通对象的返回值来源于return checkResolve(readOrdinaryObject(unshared));
,而枚举类对象的返回值来源于return checkResolve(readEnum(unshared));
。但是为了严谨起见,我们还是先考察变量tc
的产生方式。
考察bin.peekByte()
的含义
变量tc
在tc = bin.peekByte()
处被赋值,bin
是一个流对象,bin.peekByte()
的大意是获取流对象中的一个字节,而且该字节恰好是文件的第一个字节。
这里设计到了序列化时的一点源码,在ObjectOutputStream
的序列化方法writeObject0()
中,我们能过够看到这样的语句,其中obj
就是我们传入的对象。可以看到,根据obj
类型的不同,ObjectOutputStream
会采取不同的序列化方法。
// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
我们主要考察writeEnum()
与writeOrdinaryObject()
这两个方法。
在writeEnum()
方法中,我们发现代码的第一行就是bout.writeByte(TC_ENUM);
;而在writeOrdinaryObject()
中也存在首先运行bout.writeByte(TC_OBJECT);
的情况。而其中的TC_ENUM
与TC_OBJECT
恰好与之前readOnject0()
中的switch语句对应上了!
因此,我们之前的猜想是正确的:序列化后的普通对象会通过checkResolve(readOrdinaryObject(unshared))
进行反序列化;而序列化后的枚举类对象会通过checkResolve(readEnum(unshared))
进行反序列化。
readOrdinaryObject()
方法
源码中readOrdinaryObject()
比较复杂,我节选了我们关心的部分进行解读。
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
// 其它处理。。。
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
其中,变量desc
是一个ObjectStreamClass
类型的变量,它可以看作obj
的类对象的包装。
可以看到,obj
的首要生成方式为调用newInstance()
进行实例化,此时JVM会为其创建一个新的实例对象,这种做法会破坏单例模式。
但是在后续代码中,如果发现类对象存在readResolve()
方法,则会在Object rep = desc.invokeReadResolve(obj);
调用其方法并在handles.setObject(passHandle, obj = rep);
将原来的obj
替换掉。因此,我们可以通过重写readResolve()
方法的维持单例模式。
readEnum()
方法
在介绍readEnum()
之前,我们可以先看一下在ObjectOutputStream
中的writeEnum()
方法,以便于更好的理解readEnum()
。
下面是writeEnum()
方法的源码,一共就5行。其中第一行之前已经解释过了,向文件中写入了1字节的对象类型。在第三行,又写入了一个ObjectStreamClass
,这里可以理解为写入了枚举对象所在类的信息。在第五行,方法写入了枚举对象的name
属性(这里的name
指的是作为枚举类对象的属性,在实现5中指的是"INSTANCE",而并非我们定义在枚举类中的成员变量name
)。
private void writeEnum(Enum<?> en,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
bout.writeByte(TC_ENUM);
ObjectStreamClass sdesc = desc.getSuperDesc();
writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
handles.assign(unshared ? null : en);
writeString(en.name(), false);
}
综上,我们大概了解了序列化后的枚举对象的组成部分,现在回头查看readEnum()
方法的源码,可以发现一一对应之处。
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
在该源码中,方法首先通过readByte()
读取了1字节的数据并进行类型判断。
然后,方法通过readClassDesc(false)
读取了类对象的信息,并判断该类是否真实是枚举类。
最后,方法通过String name = readString(false);
读取了枚举对象的name
属性。但与readOrdinaryObject()
不同的是,方法并没有调用newInstance()
产生实例,而是通过调用Enum<?> en = Enum.valueOf((Class)cl, name);
获取枚举类中已有的对象。
综上,在整个过程中,没有新的实例对象产生,单例模式得到维护。