单例模式概念
- 有且只有一个实例
- 必须自行创建这个实例
- 必须自行向整个系统提供这个实例
单例设计
单例的创建一般分为 饿汉式单例 、 懒汉式单例 。
饿汉式单例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class EagerSingleton { private static EagerSingleton singleton = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() { return singleton; } }
|
当类被加载的时候,static会初始化,创建当前对象,因为构造函数是私有的,这样避免了外部创建改对象,通过静态方法提供了系统外部的访问。由java类加载器保证该类只会加载一次。采用的是空间换时间。
缺点:该类就是在系统中不使用也会初始化,对象会一直存在,不会被回收。占用系统资源。
在jdk中有啥用饿汉式单例设计模式,如Runtime:
懒汉式单例
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 34 35
|
class LazySingleton {
private volatile static LazySingleton singleton = null;
private LazySingleton() { }
public static LazySingleton getInstance() { if (singleton == null) { synchronized(LazySingleton.class){ if (singleton == null){ singleton = new LazySingleton(); } } } return singleton; } }
|
类被初始化的时候,不会创建对象。在第一次调用getInstance才会创建对象。第一次判断,判断是否单例已经存在。在创建改单例的时候,加锁后判断,是避免多线程访问的时候,保证只创建一个实例。
在java中java.util.Calendar使用的是懒汉式。
缺点:因为使用了volatile和判断,速度相对饿汉式要慢,懒汉式避免了未使用的时候占用系统资源。
Initialization Demand Holder (IoDH) 单例
饿汉式单例和懒汉式单例创建各有优缺点。使用IoDH可以避免二者缺点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Singleton{
private Singleton(){}
private static class HolderClass{ private final static Singleton singleton = new Singleton(); }
public static Singleton getInstance(){ return HolderClass.singleton; } }
|
在类被初始化的时候,静态内部类并不会被初始化,只有使用到静态内部类的时候,才会被初始化。因为是私有类,保证了外部无法访问,静态属性由java类加载器保证了该类只被初始化一次。同时没有锁没有判断,集合之前两种单例创建的优点。
枚举单例
之前三种单例创建有个缺点,在单例实现序列化后,在实例序列化反序列化后,会存在多个实现,违背了单例设计模式。例:
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 34 35 36 37 38 39 40 41 42 43 44
| class Singleton implements Serializable{ private static final long serialVersionUID = -1218018069415776722L;
private Singleton(){}
private static class HolderClass{ private final static Singleton singleton = new Singleton(); }
public static Singleton getInstance(){ return HolderClass.singleton; } }
public class Main { public static void main(String[] args) throws IOException, ClassNotFoundException { Singleton instance = Singleton.getInstance(); PipedOutputStream outputStream = new PipedOutputStream(); PipedInputStream inputStream = new PipedInputStream(); inputStream.connect(outputStream); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(instance);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); Singleton singleton = (Singleton) objectInputStream.readObject(); outputStream.close(); inputStream.close();
System.out.println(singleton == instance); } }
|
注:可以使用readResolve避免,但是需要自己处理
在该例子中,序列化后的对象是一个新的实例。
解决办法使用枚举做单例:
1 2 3 4 5 6
| enum EnumSingleton{ SINGLETON; public void doSomeThing(){ System.out.println("单例"); } }
|
在《高效Java 第二版》中说:单元素的枚举类型已经成为实现Singleton的最佳方法。用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。
枚举在序列化时候其实是序列化了name,在反序列化的时候,通过valueOf(name),保证了枚举的唯一性。
单例模式总结
单例因为控制实例的创建,在系统内存中只有唯一一份,可以节约系统资源,减少了对象的频繁创建和销毁。在Spring中默认创建的对象为单例,在Struts2中action默认创建的对象为多例。
单例缺点在于扩展比较困难,而且违背了“单一职责原则”,因为单例又是对象创建工厂,又是实例,还包含相关业务方法。
单例设计模式用在,系统因为消耗太大只允许创建一个对象情况。或者系统只能使用一个公共访问点,除了公共访问点,不能通过其他途径访问该实例。