说到单例模式,可能是所有23种设计模式中用的最多并且相对来说最简单的一种了,今天我们分析一下实现单例模式的不同方式,以及我们设计的单例模式有什么缺点,有什么方面需要改进等等问题,好了下面进入正题:
这个模式主要用于整个系统中只能出现一次类的实例的情况下,比如全局配置信息,在介绍单例模式之前有必要介绍单例模式和该类静态变量的区别:
单例模式:
(1)保证某类的实例是全局唯一的;
(2)由该类本身完成实例化;
(3)实例化该类实例的方法是确定的,比如getInstance静态方法;
该类静态变量:
(1)该类的实例引用的静态变量可以出现在任何类中;
(2)任何位置都可以对该变量进行重新赋值,根本达不到单例的目的;
比较完之后开始讲解单例模式实现的几种方法:
方法1:单线程环境中(懒汉式)
public class SingletonMode {
private static SingletonMode instance = null;
private SingletonMode(){};
public static SingletonMode getInstance()
{
if(instance == null)
instance = new SingletonMode();
return instance;
}
}
需要注意的一点就是单例模式中的类构造器是私有的,这样的话就不允许外界随便创建这个类的实例;
第一种方法是适用于单线程环境中的懒汉式加载,因为instance实例的创建是在显式调用getInstance方法之后才会创建的;但是在多线程环境下这种方法是行不通的,加入现在有ThreadA 和 ThreadB 两个线程,同时执行了if(instance == null)的判断并且都判定instance为null的话,都会执行new SingletonMode( )方法,这样的话必定有一个会刷掉前一个创建的实例,但是这个顺序也不能保证,因而造成了instance的值并不是唯一的,违背了单例模式的设计初衷,因此不适用于多线程环境下;
方法2:多线程环境中(懒汉式)
上面不适用于多线程环境中的原因是在判断instance是否为空的操作时,我们并没有进行加锁操作,因此很直观的我们想到了可以对这个判断进行加锁操作;
public class SingletonMode {
private static SingletonMode instance = null;
private SingletonMode(){};
public static SingletonMode getInstance()
{
synchronized (SingletonMode.class) {
if(instance == null)
instance = new SingletonMode();
return instance;
}
}
}
这种方法是将锁加在了判断语句前,我们还可以把锁加在getInstance方法前面,同样也能达到单例的目的;
public class SingletonMode {
private static SingletonMode instance = null;
private SingletonMode(){};
synchronized public static SingletonMode getInstance()
{
if(instance == null)
instance = new SingletonMode();
return instance;
}
}
上面两种在多线程环境中实现单例模式的方式有一个很明显的缺点就是效率比较低,因为我们加锁的时机有点太早了,导致本来并不需要加锁的代码也进行了加锁同步操作,显然效率上就有所降低了;
方法3:双重校验锁(懒汉式)
这也就直接促成了第三种方式的产生,他也适用于多线程环境中,并且他的锁加载时机是正合适的;
public class SingletonMode {
private static SingletonMode instance = null;
private SingletonMode(){};
public static SingletonMode getInstance()
{
if(instance == null)
{
//这里存在耗时操作
synchronized (SingletonMode.class) {
if(instance == null)
instance = new SingletonMode();
}
}
return instance;
}
}
可能很多人都会怀疑,为什么要进行两次instance == null的判断呢,这个不是太麻烦了吗?又加锁又多次判断instance == null,好了,下面进行分析,假设我们去掉后面一个instance == null的判断得到下面代码:
public class SingletonMode {
private static SingletonMode instance = null;
private SingletonMode(){};
public static SingletonMode getInstance()
{
if(instance == null)
{
//这里存在耗时操作
synchronized (SingletonMode.class) {
instance = new SingletonMode();
}
}
return instance;
}
}
假设现在有ThreadA 和 ThreadB 两个线程,首先ThreadA先调用getInstance方法,他判断出了instance == null ,因此进入if方法里面,此时ThreadA开始了耗时操作,相当于阻塞住一样,此时ThreadB调用getInstance方法,因为此刻ThreadA处于耗时操作中,所以instance实例是没有创建出来的依然为null,因此ThreadB也进入到了if方法里面,但是ThreadB不像ThreadA方法那样需要进行耗时操作,因此得到了锁进入到了同步代码块中,并且创建了instance实例,最后释放了锁,随后ThreadA结束了耗时操作,得到锁进入了同步代码块中,同样也创建一个instance实例,看到没有,此时又创建了一个instance实例出来了,此时创建的和之前ThreadB创建的并不是一个的,所有这里进行instance == null判断还是很有必要的;方法4:多线程环境(饿汉式)
public class SingletonMode {
private static SingletonMode instance = new SingletonMode();
private SingletonMode(){};
public static SingletonMode getInstance()
{
return instance;
}
}
上面这种方法是采用静态变量实现的,我们也可以采用静态代码块实现;
public class SingletonMode {
private static SingletonMode instance = null;
static
{
instance = new SingletonMode();
}
private SingletonMode(){};
public static SingletonMode getInstance()
{
return instance;
}
}
同样我们也可以采用静态内部类的方式来实现:
public class SingletonMode {
private SingletonMode(){};
private static class MySingleton
{
private static SingletonMode instance = new SingletonMode();
}
public static SingletonMode getInstance()
{
return MySingleton.instance;
}
}
方法4中的三种创建单例模式的方法利用了类加载机制中对static变量或者static代码块都会加载到方法区的静态数据区,这是由JVM本身来保证线程安全以及单例的,因此在效率上来说比上面提到的更高。
好了,上面四种方法分别介绍了单线程、多线程环境下饿汉式、懒汉式的单例模式,基本上是可以实现单例了,但是有一个问题出现了,在我们对这个单例类进行序列化之后,进行反序列化会发现我们又创建出一个实例出来,这种情况下同样也违背了单例模式,下面举例说明:(以多线程环境下的懒汉式为例)
import java.io.Serializable;
public class SingletonMode implements Serializable{
private SingletonMode(){};
private static SingletonMode instance = new SingletonMode();
public static SingletonMode getInstance()
{
return instance;
}
}
某各类需要序列化,只要实现Serializable接口即可;
测试类:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SingletonModeTest {
public static void main(String[] args) {
SingletonMode instance = SingletonMode.getInstance();
SingletonMode anotherInsance = null;
//将instance序列化到磁盘
try {
FileOutputStream fos = new FileOutputStream(new File("d:/instance.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
oos.close();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//从磁盘反序列化出instance
try {
FileInputStream fis = new FileInputStream(new File("d:/instance.txt"));
ObjectInputStream ois = new ObjectInputStream(fis);
anotherInsance = (SingletonMode) ois.readObject();
ois.close();
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//判断两个对象是否相等
System.out.println(instance == anotherInsance);//false
}
}
可以发现,输出结果为false,显然在序列化反序列化之后不符合单例模式,我们的解决方法是在单例类中添加readResolve函数,这个函数也是私有的,并且返回的是单例实例;
修改之后的单例类:
import java.io.Serializable;
public class SingletonMode implements Serializable{
private SingletonMode(){};
private static SingletonMode instance = new SingletonMode();
public static SingletonMode getInstance()
{
return instance;
}
private Object readResolve()
{
return instance;
}
}
这样再次执行上面的测试函数,发现输出为true,解决问题;
解决完序列化问题之后,新的问题出来了,在我们进行反射创建新的类实例的时候同样也会破坏单例条件,因为在反射创建类的时候,我们可以通过setAccessible将私有构造
函数设置为允许访问,这样的结果就是我们可以随便访问私有构造器,将单例模式的构造器暴露出来了,下面举例说明:
我们还是以上面测试序列化的单例类为例进行观察:
public class SingletonMode{
private SingletonMode(){};
private static SingletonMode instance = new SingletonMode();
public static SingletonMode getInstance()
{
return instance;
}
}
测试类:
import java.lang.reflect.Constructor;
public class SingletonModeTest {
public static void main(String[] args) {
try {
Constructor constructor = SingletonMode.class.getDeclaredConstructor();
//设置可以访问私有构造器
constructor.setAccessible(true);
//通过反射获取实例
SingletonMode instance1 = (SingletonMode) constructor.newInstance();
SingletonMode instance2 = (SingletonMode) constructor.newInstance();
System.out.println(instance1 == instance2);//false
} catch (Exception e) {
e.printStackTrace();
}
}
}
看到没有,输出结果为false,表示instance1和instance2是并不相等的,这也是上面所有创建单例类所共同存在的问题,这种通过反射破坏单例的方法称为反射攻击,避免措施是在我们进行第二次创建的时候抛出异常,告诉程序员已经创建了一个实例;这种思想也是在Effective java中所提到的枚举类型实现单例模式避免反射攻击的措施;
下面先看看用枚举实现单例模式的真面目,至于枚举是怎么保证能运用于多线程环境稍后在源码中也能发现,他也是利用ClassLoader类加载机制加载static的特殊性实现的,随后从源码角度具体进行分析枚举到底是通过什么方法来避免序列化以及反射攻击破坏单例模式的;
public enum SingletonMode{
instance;
}
接着我们通过反射的方式来创建两个单例实例,来看看会发生什么:
public class SingletonModeTest {
public static void main(String[] args) {
try {
Constructor constructor = SingletonMode.class.getDeclaredConstructor();
//设置可以访问私有构造器
constructor.setAccessible(true);
//通过反射获取实例
SingletonMode instance1 = (SingletonMode) constructor.newInstance();
SingletonMode instance2 = (SingletonMode) constructor.newInstance();
System.out.println(instance1 == instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果为:
java.lang.NoSuchMethodException: com.hzw.day32.SingletonMode.<init>()
at java.lang.Class.getConstructor0(Class.java:2892)
at java.lang.Class.getDeclaredConstructor(Class.java:2058)
at com.hzw.day32.SingletonModeTest.main(SingletonModeTest.java:8)
可以发现提示我们根本没有无参构造函数存在,通过SingletonMode.class.getDeclaredConstructor()(注意:getDeclaredConstructor返回的是所有的构造函数包括public 和非public的,而getConstructor仅仅返回public 的构造函数,也就是getDeclaredConstructor的子集)来获取到SingletonMode里面存在的构造函数,输出结果是private com.hzw.day32.SingletonMode(java.lang.String,int),也就是说枚举类型实现的单例模式中是不存在无参构造器的,只存在参数为String和int的private构造器,这两个参数实际上就是枚举的name和ordinal属性,也就是枚举的名字和所在枚举中的位置,这一点可以从下面对枚举单例模式.class反编译之后的源码中看到;
public final class SingletonMode extends Enum
{
private SingletonMode(String s, int i)
{
super(s, i);
}
public static SingletonMode[] values()
{
SingletonMode asingletonmode[];
int i;
SingletonMode asingletonmode1[];
System.arraycopy(asingletonmode = ENUM$VALUES, 0, asingletonmode1 = new SingletonMode[i = asingletonmode.length], 0, i);
return asingletonmode1;
}
public static SingletonMode valueOf(String s)
{
return (SingletonMode)Enum.valueOf(com/hzw/day32/SingletonMode, s);
}
public static final SingletonMode instance;
private static final SingletonMode ENUM$VALUES[];
static
{
instance = new SingletonMode("instance", 0);
ENUM$VALUES = (new SingletonMode[] {
instance
});
}
}
既然调用无参构造器会提示我们不存在这样的构造器,那么我们可以调用他的有参构造器,因为反射是可以调用私有构造器的,所以按理说我们可以通过有参构造器破坏枚举单例类的,测试代码如下:
import java.lang.reflect.Constructor;
public class SingletonModeTest {
public static void main(String[] args) {
try {
Constructor constructor = SingletonMode.class.getDeclaredConstructor(String.class,int.class);
//设置可以访问私有构造器
constructor.setAccessible(true);
//通过反射获取实例
SingletonMode instance1 = (SingletonMode) constructor.newInstance("hu",23);
SingletonMode instance2 = (SingletonMode) constructor.newInstance("zhi",24);
System.out.println(instance1 == instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果为:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:521)
at com.hzw.day32.SingletonModeTest.main(SingletonModeTest.java:12)
看到没有呢?这里输出的异常和上面的完全不一样,意思是我们不能反射出枚举实例,也就是枚举是直接拒绝你进行反射操作的,这个异常是从哪里抛出来的呢?要想明白这点,我们需要查看下Constructor的源码,因为错误是报在Constructor类的521行,源码如下:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
return (T) ca.newInstance(initargs);
}
关键代码:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
也就是说只要类的定义中包含有Enum关键字都会拒绝其反射操作的,从上面的反编译的枚举源码中可以看出我们的SingletonMode单例类是继承自Enum的;
这下搞清楚为什么枚举能够避免反射攻击了吧,其实就是直接判断需要反射的类是否继承自枚举,如果是的话就抛出异常来实现的;
好了,搞清楚枚举避免反射攻击原因之后,接下来的问题就是枚举是怎么避免序列化反序列化之后生成新的对象的呢?别急,马上从源码角度分析:
要想搞清楚这个问题,首先需要知道的就是java本身对枚举是做了很多限制的,在序列化的时候仅仅是将枚举对象的name属性存储到结果中,并不会像普通序列化过程中将name和ordinal全部存储进去,反序列化的时候是通过java.lang.Enum的valueOf( )方法根据名字name来查找枚举对象的,此外编译器对枚举的序列化做了跟严格的限制,不允许我们自定义序列化的过程,禁用了writeObject、readObject、readObjectNoData、writeReplace、和readResolve等方法;
那么首先第一步,我们就应该看java.lang.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);
}
首先通过Class对象的enumConstantDirectory方法获得map中名字为name的对象,如果为空抛出异常,进一步,我们看看enumConstantDirectory的源码:
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;
}
这里面最关键的就是通过getEnumConstantsShared方法来获取枚举数组,这个方法是通过反射的方式获取数组的,源码如下:
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;
}
});
enumConstants = (T[])values.invoke(null);
}
// These can happen when users concoct enum-like classes
// that don't comply with the enum spec.
catch (InvocationTargetException ex) { return null; }
catch (NoSuchMethodException ex) { return null; }
catch (IllegalAccessException ex) { return null; }
}
return enumConstants;
}
这里面关键代码是final Method values = getMethod("values");这个就是通过反射的方式获取枚举类中的values方法,并且通过invoke方法反射调用,因此这就回到了我们反编译出来的values方法中啦,这个方法的作用是将之前用static块创建的ENUM$VALUES值复制到SingletonMode数组中并且返回这个数组,个人感觉values方法和我们自己实现反序列化时采用的readResolve函数作用是一致的,至于枚举为什么使用values方法,还不是太明白;
好啦,高大上的枚举实现单例模式已经介绍结束,有没有觉得多少了解了点呢?
perfect,至此,关于单例模式中遇到的基本问题已经介绍完毕啦,但是,还有一种情况没有考虑进去,那就是如果我们采用不同的类加载器来加载相同的单例类的.class字节码文件的时候,同样会破坏单例,在这里我们不能使用高大上的枚举类型进行测试,因为枚举单例是禁止反射的,可以采用多线程饿汉式进行分析;
单例类:
public class SingletonMode{
private static SingletonMode instance = new SingletonMode();
private SingletonMode(){};
public static SingletonMode getInstance()
{
return instance;
}
}
自定义类加载器:
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
public class MyClassLoader extends ClassLoader {
public String path;
public MyClassLoader() {
}
public MyClassLoader(String path)
{
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] result = getClassBytes(name);
return this.defineClass("com.hzw.day32.SingletonMode", result, 0,result.length);
}
public byte[] getClassBytes(String path)
{
byte[] result = null;
try {
FileInputStream fis = new FileInputStream(new File(path));
BufferedInputStream bis = new BufferedInputStream(fis);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int c = bis.read();//读取bis流的下一个字节
while(c != -1)
{
baos.write(c);
c = bis.read();
}
bis.close();
result = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
测试类:
import java.lang.reflect.Constructor;
public class SingletonModeTest {
public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader();
SingletonMode instance = SingletonMode.getInstance();
Object anotherInstance = null;
try {
Class anotherClass = myClassLoader.findClass("D:/DemoProjects/ProgrammingTest/bin/com/hzw/day32/SingletonMode.class");
Constructor constructor = anotherClass.getDeclaredConstructor();
constructor.setAccessible(true);
anotherInstance = constructor.newInstance();
System.out.println("instance: "+instance);
System.out.println("anotherIntsnace: "+anotherInstance);
System.out.println(instance == anotherInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果:
instance: com.hzw.day32.SingletonMode@4614ac54
anotherIntsnace: com.hzw.day32.SingletonMode@773de2bd
false
其实说白了,类加载器破坏单例实质上就是反射破坏单例,这也就是为什么枚举类型不会受到破坏的原因了;
至此,个人认为的单例知识点总结完毕;