说说Java单例设计模式
说说Java单例设计模式
第一次写博客,请各位多指教。
单例模式是Java中的一种设计模式,指在设计一个类时,需要保证在整个程序运行期间针对该类只存在一个实例。就好像我们生活中一些单例,比如:只能有一个太阳,一个月亮。
设计思路
设计一个单例类,无论用哪种方法实现,都无外乎三步:
1.构造方法私有化,这是为了不让外界通过new的形式产生对象;
2. 在单例类里面自己初始化一个实例;
3. 对外暴露一个方法获取这个实例;
只要实现的上面所说的三步,我们就能实现一个模式。
简单入门
class Single {
//1.构造方法私有化
private Single(){}
//2.自己创建一个对象
private static Single INSTANCE=new Single();
//3.对外暴露获取方法
public static Single getInstance(){
return INSTANCE;
}
}
//此时,通过代码测试可以发现输出为true;
Single s1=Single.getInstance();
Single s2=Single.getInstance();
System.out.println(s1==s2);
版本升级
上面实现一个简单的单例模式,但其实上面的单例模式只是一个简单的入门,通过观察可以发现上面的单例其实可以发现,单例对象没有实现懒加载,是一个饿汉式的单例,下面给出了2种饿汉式的单例:
class Single{
private Single() {};
private static class LazyHolder{
private static final Single INSTANCE=new Single();
}
public static final Single getInstance() {
return LazyHolder.INSTANCE;
}
}
这种形式的懒汉式的单例,利用了内部类的特点:内部类不会随着外部类的加载而加载。这样在内部类里面实现外部单例类对象的建立,在用户第一次使用LazyHolder这个内部类的时候,就会先去初始化单例对象,这样就实现了懒加载。而且这种方式,不会存在线程问题,因为类在加载的时候是线程安全的,所以实例只会被创建一次。
下面看用双重检查实现:
class Singleton4{
private Singleton4() {}
private static volatile Singleton4 INSTANCE=null;
public static Singleton4 getInstance() {
if(INSTANCE==null) {
synchronized(Singleton4.class) {
if(INSTANCE==null) {
INSTANCE=new Singleton4();
}
}
}
return INSTANCE;
}
}
这种双重检查的方式,实现了单例,也实现了懒加载,但是要小心的是,一定要加上volatile这个关键词,因为在创建的对象的时候,大概是这三步:
1:分配对象的内存空间;
2:初始化对象;
3:把INSTANCE指向分配的内存空间;
那么其实这三步是有可能被重排序的,如果被重排序就会出现下面这种情况:
1:分配内存空间;
2:把INSTANCE指向分配的内存空间;
3:初始化对象;
一旦发生这样的重排序,那么就有可能第一个线程在第二步时停住了,此时第二个线程到达,他进行第一次检查,因为已经分配了内存空间,所以INSTANCE不为空,那么就会直接返回INSTANCE,但此时的INSTANCE是没有初始化的,那就会出错。但是一旦加上了volatile关键字,就会防止出现重排序。
问题没完
但其实上面说的方法都是有漏洞的,都会被反射和序列化破坏,破坏方法如下:
Singleton4 instance1 = Singleton4.getInstance();
Singleton4 instance2 = Singleton4.getInstance();
//这个依然会输出true,因为还没被破坏
System.out.println(instance1==instance2);
//通过反射,得到Singleton4的构造函数,
Class<?> forName = Class.forName("Singleton4");
Constructor<?> constructor = forName.getDeclaredConstructor();
//这里会输出false,isAccessible的意思是可访问的,这是因为我们把构造方法私有化了,所以会输出false;
System.out.println(constructor.isAccessible());
//这里的操作是把构造方法设置会可访问的
constructor.setAccessible(true);
//进过上面的设置,这里就会输出true
System.out.println(constructor.isAccessible());
//这里就可以通过newInstance()来产生一个新的实例,就会输出false
System.out.println(constructor.newInstance()==instance2);
下面是用序列化破坏,代码如下:
FileOutputStream fileOut =new FileOutputStream("A://Single.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(Singleton5.getInstance());
FileInputStream fileIn = new FileInputStream("A://Single.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Singleton5 INSTANCE=(Singleton5) in.readObject();
System.out.println(INSTANCE==Singleton5.getInstance());
这片代码的意思就是,把一个单例序列化到本地,然后再反序列化读到内存中来,结果会输出false,可见这个单例被序列化破坏了,原因就是反序列化也是通过反射来建立新的对象,所以被破坏了。
那么,解决这个问题方法就是用枚举来实现单例,用枚举实现单例很简单代码如下:
public enum SingletonClass {
INSTANCE;
public static SingletonClass getInstance() {
return INSTANCE;
}
}
没错就是这么简单就实现的单例,因为枚举在Java中天生就是如此,不用过多的其他操作,如果此时再用上面反射的方法去试图破坏话,会直接报错,因为Java在设计枚举的时候就屏蔽了这种操作。而用序列化去破坏呢,也会输出true的,所以发现序列化不会破坏枚举的单例。
以上就是本次的全部内容,真心希望错误的地方各位能多多指教。感谢,阿门。