Java面试经验第一季之设计模式——单例模式

Java面试经验第一季之设计模式——单例模式

面试

单例模式,主要作用是保证在Java程序中,某个类只有一个实例存在,在Java中一些管理器和控制器就被设计成单例模式,在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。

单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。

单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、双重校验锁单例。

单例模式有以下特点:

  1、单例类只能有一个实例。

  2、单例类必须自己创建自己的唯一实例。

  3、单例类必须给所有其他对象提供这一实例。

重点来了,当你自己写单例的时候,记住三点:私有的对象声明;私有的对象构造器;公有的获取实例方法。在你面试的时候,不管面试官让你口述或者写出来,你把这三点表述出来肯定就是一个单例。当然这仅仅是一个非常简单的单例,是一个单例的基础框架,面试官肯定会让你深入说明,而更深层的东西无外乎两点:线程安全和延迟加载。根据延迟加载,单例可划分为懒汉式单例和饿汉式单例。在讲解过程中我会把线程安全穿插进去。

饿汉式:

饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。

  1. publicclass Singleton{

  2. private static Singleton instance = new Singleton(); //私有的对象声明

  3. private Singleton(){} //私有的对象构造器

  4. public static Singleton newInstance(){ //公有的获取实例方法

  5. return instance;

  6. }

  7. }

懒汉式:

在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象,致命的是在多线程不能正常工作。

  1. publicclass Singleton{

  2. private static Singleton instance = null; //私有的对象声明

  3. private Singleton(){} //私有的对象构造器

  4. public static Singleton newInstance(){ //公有的获取实例方法

  5. if(null == instance){

  6. instance = new Singleton();

  7. }

  8. return instance;

  9. }

  10. }

这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题,即在获取实例方法的时候,添加同步锁synchronized ,此时该懒汉式单例为线程安全的,遗憾的是,效率很低,99%情况下不需要同步。

  1. publicclass Singleton{

  2. private static Singleton instance = null;

  3. private Singleton(){}

  4. public static synchronized Singleton newInstance(){

  5. if(null == instance){

  6. instance = new Singleton();

  7. }

  8. return instance;

  9. }

  10. }

双重校验锁:

加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。

  1. publicclass Singleton {

  2. private volatile static Singleton instance = null;

  3. private Singleton(){}

  4. public static Singleton getInstance() {

  5. if (instance == null) {

  6. synchronized (Singleton.class) {

  7. if (instance == null) {

  8. instance = new Singleton();

  9. }

  10. }

  11. }

  12. return instance;

  13. }

  14. }

将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。

静态内部类:

实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式:

  1. publicclass Singleton {

  2.  
  3. /* 私有构造方法,防止被实例化 */

  4. private Singleton() {

  5. }

  6.  
  7. /* 此处使用一个内部类来维护单例 */

  8. privatestaticclass SingletonFactory {

  9. privatestatic Singleton instance = new Singleton();

  10. }

  11.  
  12. /* 获取实例 */

  13. publicstatic Singleton getInstance() {

  14. return SingletonFactory.instance;

  15. }

  16. }

其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。也有人这样实现:因为我们只需要在创建类的时候进行同步,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字,也是可以的:

  1. publicclass SingletonTest {

  2.  
  3. private static SingletonTest instance = null;

  4.  
  5. private SingletonTest() {

  6. }

  7.  
  8. private static synchronized void syncInit() {

  9. if (instance == null) {

  10. instance = new SingletonTest();

  11. }

  12. }

  13.  
  14. public static SingletonTest getInstance() {

  15. if (instance == null) {

  16. syncInit();

  17. }

  18. return instance;

  19. }

  20. }

当然为了保证序列化时,前后对象一致,以上四种单例都可以添加一个方法。

  1. /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */

  2. public Object readResolve() {

  3. return instance;

  4. }

枚举:

  1. public enum Singleton{

  2. instance;

  3. public void whateverMethod(){}

  4. }

上面提到的四种实现单例的方式都有共同的缺点:

1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。

2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。

总结

本文总结了五种Java中实现单例的方法,其中前两种都不够完美,双重校验锁和静态内部类的方式可以解决大部分问题,平时工作中使用的最多的也是这两种方式。枚举方式虽然很完美的解决了各种问题,但是这种写法多少让人感觉有些生疏。个人的建议是,在没有特殊需求的情况下,使用双重校验锁和静态内部类方式实现单例模式。

欢迎大家指出不足之处,供大家交流进步,麻烦大家多多评论和关注我,想了解更多,公众平台:程序传说哥

猜你喜欢

转载自blog.csdn.net/qq_41552245/article/details/87867381