一、概述
1、介绍
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
2、优缺点
优点:提供了对唯一实例的受控访问;由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时,单例模式无疑可以提高系统的性能;避免对共享资源的多重占用。
缺点:不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态;由于单利模式中没有抽象层,因此单例类的扩展有很大的困难;单例类的职责过重,在一定程度上违背了"单一职责原则"。
3、应用场景
网站的计数器;
Web应用用于读取配置文件的类,因为配置文件是共享的资源;
数据库连接池的设计,因为数据库连接是一种数据库资源;
Spring中,每个bean默认都是单例的,这样便于Spring进行管理。
多线程的线程池的设计,这是由于线程池要方便对池中的线程进行控制。
二、创建方式
1、饿汉式(静态常量)(线程安全)
1 public class Singleton { 2 3 private static final Singleton instance = new Singleton(); 4 5 private Singleton() { 6 } 7 8 public static Singleton getInstance() { 9 return instance; 10 } 11 }
优点:类初始化时,会立即加载该对象,仅实例化一次。效率高,获取实例的速度快,线程是安全的。
缺点:类加载的时候立即实例化对象,可能实例化的对象不会被使用,可能造成内存的浪费。
结论:可用,不推荐。
2、饿汉式(静态代码块)(线程安全)
1 public class Singleton { 2 3 private static final Singleton instance; 4 5 private Singleton() { 6 } 7 8 static { 9 instance = new Singleton(); 10 } 11 12 public static Singleton getInstance() { 13 return instance; 14 } 15 16 }
优点:在类装载的时候,就执行静态代码块中的代码,初始化类的实例。线程是安全的。
缺点:类加载的时候立即实例化对象,可能实例化的对象不会被使用,可能造成内存的浪费。
结论:可用,不推荐。
3、懒汉式(线程不安全)
1 public class Singleton { 2 3 private static Singleton instance; 4 5 private Singleton() { 6 } 7 8 public static Singleton getInstance() { 9 if (instance == null) { 10 instance = new Singleton(); 11 } 12 13 return instance; 14 } 15 }
优点:使用的时候,创建对象,节省系统资源。
缺点:线程不安全。
结论:不可用。
4、懒汉式(线程安全,同步方法)
1 public class Singleton { 2 private static Singleton instance; 3 4 private Singleton() { 5 } 6 7 public static synchronized Singleton getInstance() { 8 if (instance == null) { 9 instance = new Singleton(); 10 } 11 return instance; 12 } 13 }
优点:线程安全。
缺点:有同步锁,效率低。
结论:可用,不推荐。
5、懒汉式(线程安全,同步代码块)
1 public class Singleton { 2 private static Singleton instance; 3 4 private Singleton() { 5 } 6 7 public static Singleton getInstance() { 8 synchronized (Singleton.class) { 9 if (instance == null) { 10 instance = new Singleton(); 11 } 12 } 13 14 return instance; 15 } 16 }
优点:线程安全。
缺点:有同步锁,效率低。
结论:可用,不推荐。
6、双重锁(线程安全)
1 public class Singleton { 2 private static volatile Singleton instance; 3 4 private Singleton() { 5 } 6 7 public static Singleton getInstance() { 8 if (instance == null) { 9 synchronized (Singleton.class) { 10 if (instance == null) { 11 instance = new Singleton(); 12 } 13 } 14 } 15 return instance; 16 } 17 }
优点:线程安全。当实例存在的时候,可用不走同步锁,减少使用锁带来的性能的消耗。延迟加载,效率较高。
缺点:无。
结论:可用,推荐。
对volatile的说明(重要):
不加volatile有可能会进行指令重排序。指令重排:一般而言初始化操作并不是一个原子操作,而是分为三步:
step1:在堆中开辟对象所需空间,分配地址。
step2:根据类加载的初始化顺序进行初始化。
step3:将内存地址返回给栈中的引用变量。
由于 Java 内存模型允许"无序写入",有些编译器因为性能原因,可能会把上述步骤中的 2 和 3 进行重排序,顺序就成了:
step1:
step3:(此时的 instance 就不是null,但变量并没有初始化完成)。
step2:
所以就可能会出现以下情况(有问题):
时间轴
|
Thread1
|
Thread2
|
1
|
第一次检测, instance 为空
|
|
2
|
获取锁
|
|
3
|
再次检测, instance 为空
|
|
4
|
在堆中分配内存空间
|
|
5
|
instance 指向分配的内存空间
|
|
6
|
第一次检测,instance不为空
|
|
7
|
访问 instance(此时对象还未初始化完成)
|
总结:不加volatile,会有问题!加入volatile关键字修饰之后,会禁用指令重排,这样就保证单例的正确性。关于更多volatile,请学习JMM。
7、静态内部类(线程安全)
1 public class Singleton { 2 3 private Singleton() { 4 } 5 6 private static class SingletonInstance { 7 private static final Singleton INSTANCE = new Singleton(); 8 } 9 10 public static Singleton getInstance() { 11 return SingletonInstance.INSTANCE; 12 } 13 }
这种方式采用了类装载的机制来保证初始化实例时只有一个线程。静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时。调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:线程安全(不会被反射入侵)。延迟加载,效率高。
缺点:需要两个类完成,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久代的对象。
结论:可用,推荐。
8、枚举(线程安全)
1 public enum Singleton { 2 INSTANCE; 3 4 public void sayOK() { 5 System.out.println("ok~"); 6 } 7 }
枚举本身就是单例的,一般在项目中定义常量。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。这种方式是Effective Java作者Josh Bloch 提倡的方式 。
结论:可用,推荐。
三、单例的破坏
单例模式一定能保证只有一个实例对象吗?答案:不能。
破坏单例的两种方式:反射、反序列化。
1、反射破坏
通过反射是可以破坏单例的,例如使用内部类实现的单例。通过反射获取其默认的构造函数,然后使默认构造函数可访问,就可以创建新的对象了。
代码示例:反射破坏单例
1 public class Main { 2 public static void main(String[] args) throws Exception { 3 Singleton instance = Singleton.getInstance(); 4 5 final Class<Singleton> aClass = Singleton.class; 6 7 // 获取默认的构造方法 8 Constructor<Singleton> constructor = aClass.getDeclaredConstructor(); 9 // 使默认构造方法可访问 10 constructor.setAccessible(true); 11 12 // 创建对象 13 final Singleton instance1 = constructor.newInstance(); 14 15 System.out.println(instance == instance1); 16 } 17 } 18 19 // 结果 20 false
代码示例:阻止反射破坏单例
1 public class ReflectionSingleton { 2 3 // 标志位 4 private static boolean flag = false; 5 6 private static ReflectionSingleton instance; 7 8 private ReflectionSingleton() { 9 synchronized (ReflectionSingleton.class) { 10 if (!flag) { 11 flag = true; 12 } else { 13 throw new RuntimeException("单例模式被破坏!"); 14 } 15 } 16 } 17 18 public static ReflectionSingleton getInstance() { 19 if (instance == null) { 20 instance = new ReflectionSingleton(); 21 } 22 23 return instance; 24 } 25 }
注意:上面可以阻止单例的破坏,但是有一个BUG,如果先用的反射,再用getInstance()获取单例,就会报错。这种写法除非能保证getInstance先于反射执行。
代码示例:反射先于获取单例的形式
1 public class Main2 { 2 public static void main(String[] args) throws Exception { 3 4 final Class<ReflectionSingleton> aClass = ReflectionSingleton.class; 5 6 // 获取默认的构造方法 7 Constructor<ReflectionSingleton> constructor = aClass.getDeclaredConstructor(); 8 // 使默认构造方法可访问 9 constructor.setAccessible(true); 10 11 // 创建对象 12 ReflectionSingleton instance2 = constructor.newInstance(); 13 System.out.println("反射实例:" + instance2); 14 15 // 再次调用 16 ReflectionSingleton instance = ReflectionSingleton.getInstance(); 17 System.out.println(instance == instance2); 18 } 19 } 20 21 // 结果 22 Exception in thread "main" java.lang.RuntimeException: 单例模式被破坏!
2、反序列化破坏
Singleton 要实现Serializable接口。
代码示例:反序列化破坏单例
1 public class Main { 2 public static void main(String[] args) throws Exception { 3 // 序列化 4 Singleton instance1 = Singleton.getInstance(); 5 6 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempfile.txt")); 7 out.writeObject(Singleton.getInstance()); 8 9 ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("tempfile.txt"))); 10 11 // 调用readObject()反序列化 12 Singleton instance2 = (Singleton) in.readObject(); 13 14 System.out.println(instance1 == instance2); // 结果是:false 15 } 16 } 17 18 // 结果 19 false
代码示例:阻止反序列化破坏单例
只需要在Singleton类中,实现自己的readResolve()方法即可。
1 public Object readResolve() { 2 return instance; 3 }