1.最近有复习了一下23种设计模式,之前随便学了学,然后记得不是很清楚了,暑假需要做一些东西,再来复习一下吧。设计模式是一种程序设计的套路,为了解决特定的问题的一般模式,所以独立于语言,我这次用的是JAVA实现的。

2.首先是最简单的单例模式,什么是单例?顾名思义就是只有一个实例,也就是说一个类只能定义一个对象。这种设计模式就是单例模式,实际上单例应用十分的广泛,比如一个很简单的例子,我们电脑上的回收站,不管你点击多少次,他都是只出现一个窗口,这个就是一个单例,另外在ssh框架中也大量的使用了这种设计模式。

3.根据单例的要求,首先我们需要私有化构造器,如果不这样,那么显然其他函数可以显示调用构造函数来构造对象,其次我们需要有一个静态的对象,也叫做公共的对象,这个对象就是单例,最后,我们需要提供这个对象的访问方法。基于这种要求,可以有多种实现方式,现在基本上有五种实现。

4.饿汉式单例模式:

package Single;

public class Singleton implements Serializable {
private Singleton(){};
private static Singleton instance=new Singleton();//类初始化时,立即加载。所以不能延时加载
public static Singleton getInstance(){return instance;}
}

饿汉式就是说比较饿,在类加载的时候,就直接构建对象。这里有很多问题,首先我们知道,这种方式是线程安全的,因为类加载是由类加载器完成的,类加载器在加载类的时候会保证线程安全。其次,这种模式不能实现延时加载,这就存在效率问题,如果加载了这个类而没有用,并且加载类花费时间较多,那么就是不划算的。所以为了解决这个问题:就出现了懒汉式加载。

5.懒汉式加载:

package Single;

public class Singleton2 {
private static Singleton2 singleton;
private Singleton2(){};
public static synchronized Singleton2 getInstance(){
if(singleton==null)
singleton=new Singleton2();
return singleton;
}
}

这是单例的懒汉式实现,我们可以看出来,只有这个对象被访问的时候,才会构建它。但是我们也可以看出来,这里加了同步锁。这就涉及到了线程同步问题,如果不加锁,在多线程模式下可能会出错(很显然的一种情况,当线程A执行到if时被挂起,线程B顺利new了一个对象,到了线程A它又会new一个对象,这就出现了两个对象)。所以需要同步锁。加了同步锁之后,在高并发情况下,就会出现线程之间互相等待,效率降低。

6.从上面我们基本上可以看出来,懒汉式和饿汉式这两种实现单例的方式,各有优缺点,所以适用于不同的场景。但是有没有一种实现,可以兼容二者的有点,同时规避缺点呢?其实是有的。大概有三种实现。

7.第一种是双重检测锁,这个就不多说了,因为这种东西其实就是把懒汉式的同步锁继续细化,只在第一次构建对象时才加锁,显然如果这个对象被任意一个线程构建出来,就不需要加锁了,直接返回。但是这种模式在编译优化以后,变成了JVM不支持的类型。所以不多说了。

8.第二种是静态内部类。

package Single;

public class Singleton3 {
private static class SinglentonClassInstance{
private static final Singleton3 instance=new Singleton3();
}
private Singleton3(){};
public static Singleton3 getInstance()
{
return SinglentonClassInstance.instance;
}
}

分析一下这个代码,首先构造器私有化了,然后定义了一个静态的内部类,我们知道,在访问一个static变量,只有真正定义它的类才会被加载。这个加载是由类加载器完成,可以保证线程安全,并且是一个final常量,所以所有线程访问的都是一个对象。这种实现,既可以实现延时加载,也可以在多线程模式下保证线程安全。是一种比较完美的实现。

8.第三种是枚举类。

package Single;

/*
枚举类实现单例。
*/
public enum Singleton4 {
INSTACNE;//这个枚举元素本身就是一个单例对象
//其他的操作
public void otherOperation(){};
}

定义一个枚举类,由于枚举JVM的底层实现,他就是一个天然的单例。这也是最简单的实现方式,具有线程安全和支持延时加载两种特性。

9.最后,说一下单例的一些漏洞。单例最重要的就是只能有一个对象,然而,通过一些特殊的方法可以实现构建多个对象,即使构造器私有化了也不行,这些叫做单例存在的漏洞。我们来看一下这些漏洞以及避免方法。

10.通过反射加载类。我们知道,反射可以加载这个类的所有方法,即使私有成员,依然是可以访问到的,我们可以跳过访问权限检查,就可以访问到和使用私有对象。例如:我们用Singleton为例:

package Single;

import java.lang.reflect.Constructor;

public class Test {
public static void main(String[] args) {
// Single.Singleton3 s1=Single.Singleton3.getInstance();
// Single.Singleton3 s2=Single.Singleton3.getInstance();
// System.out.println(s1);
// System.out.println(s2);
try {
Class<Singleton>clazz=(Class<Singleton>) Class.forName("Single.Singleton");
Constructor<Singleton> constructor=clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);//跳过访问检查
Singleton s2=constructor.newInstance();
Singleton s3=constructor.newInstance();
System.out.println(s2);
System.out.println(s3);
}catch (Exception e)
{
e.printStackTrace();
System.out.println(e);
}
}
}

这里构建的s2和s3就是两个对象。其实避免它的方法很简单,我们在构造函数里面抛出异常即可。

private  Singleton()
{
if(instance!=null)
throw new RuntimeException();
};

例如我们在构造函数里面检查单例是不是空的,如果不是,那么就抛出异常,这样当外部程序试图通过反射调用构造器来构造新对象时,程序就会崩溃。

11.通过序列化和反序列化来构建对象。我们首先把对象序列化到磁盘上,然后反序列化它,这样其实就会出现两个对象。底层实现我还不懂为什么,但是这也肯定是通过反射实现的。

package Single;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/*
通过反序列化构造多个对象。
*/
public class Test2 {
public static void main(String[] args) {
try {
Singleton s1=Singleton.getInstance();
FileOutputStream fos = new FileOutputStream("d://a.txt");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(s1);
fos.close();
oos.close();

ObjectInputStream ors=new ObjectInputStream(new FileInputStream("D://a.txt"));
Singleton s2=(Singleton) ors.readObject();
System.out.println(s1);
System.out.println(s2);
}catch (Exception e)
{
e.printStackTrace();
System.out.println(e);
}
}
}

当然,前提是这个类必须支持序列化,也就是实现Serializable接口。这种漏洞的解决方案也很简单,在类里面定义一个回调时的函数:

private Object readResolve()throws ObjectStreamException{
return instance;
}

这样,在反序列化时就会返回这个对象,不会重新构建。

单例基本上也就这些东西了,其实理论好懂,重在实践。重要的时在解决问题想到使用单例来解决问题。