概述
单例模式(SingletonPattern),保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式有 3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点;
在很多比较大型的程序中,全局变量经常被用到。如果不用全局变量,那么在使用到的模块中,都需要用参数将全局变量传入,这是非常麻烦的。虽然要减少使用全局变量,但是如果需要,还是要用。单例模式就是对传统的全局的一种改进。单例可以做到延时实例化,即在需要的时候才进行实例化。针对一些大型的类,延时实例化是有好处的。
实现
饿汉式单例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1() { }
public static Singleton1 getInstance() { return instance; } }
|
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
- 场景:这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。
懒汉式单例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public class Singleton2 {
private volatile static Singleton2 instance = null;
private Singleton2() { }
public static Singleton2 getInstance() { if (instance == null) { synchronized (Singleton2.class) { if (instance == null) { instance = new Singleton2(); } } } return instance; } }
|
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁synchronized才能保证单例,(静态同步方法实现的懒汉式)加锁会影响效率。
登记式单例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public class Singleton3 {
private Singleton3() { }
private static class SingletonWrapper { private static final Singleton3 instance = new Singleton3(); }
public static Singleton3 getInstance() { return SingletonWrapper.instance; } }
|
- 优点: 内部类只有在外部类被调用才加载,产生SINGLETON实例;又不用加锁。此模式有上述两个模式的优点,屏蔽了它们的缺点,是推荐的单例模式。
- 缺点: 在实例需要序列化的场景下,反射和序列化会破坏单例,这是懒汉式、饿汉式和登记式共同存在的缺陷。
枚举单例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
public class Singleton4 {
private Singleton4() {
}
public static Singleton4 getInstance() { return Singleton.INSTANCE.getInstance(); }
private enum Singleton {
INSTANCE;
private Singleton4 singleton;
Singleton() { singleton = new Singleton4(); }
public Singleton4 getInstance() { return singleton; } } }
|
反射和反序列化对单例的影响
通过反射来实例化类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public class Singleton5 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Class<Singleton1> clz = Singleton1.class; Constructor<Singleton1> constructor = clz.getDeclaredConstructor(); constructor.setAccessible(true); Singleton1 reflectInstance = constructor.newInstance(); Singleton1 instance = Singleton1.getInstance(); System.out.println(reflectInstance == instance); } }
|
结果输出false,说明reflectInstance和instance不是同一个对象。(==比较的是实例对象的内存地址)
通过反序列化来实例化类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public class Singleton6 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton1 singleton = Singleton1.getInstance();
FileOutputStream fos = new FileOutputStream("Singleton1.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(singleton); oos.flush(); fos.close(); oos.close();
FileInputStream fis = new FileInputStream("Singleton1.obj"); ObjectInputStream ois = new ObjectInputStream(fis); Singleton1 instance = (Singleton1) ois.readObject(); fis.close(); ois.close();
System.out.println(singleton == instance);
} }
|
结果输出false,说明singleton和instance指向不同对象。
如何避免单例被破坏
修改单例类,解决反序列化的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
public class Singleton1 implements Serializable {
private static Singleton1 instance = new Singleton1();
private Singleton1() { }
public static Singleton1 getInstance() { return instance; }
protected Object readResolve() throws ObjectStreamException { System.out.println("调用了readResolve方法!"); return instance; } }
|
结果输出
应用场景
单例模式可以避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间。有以下场景的特点即可使用单例。
当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如数据库的连接池、zK分布式锁、工具类等。
当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。