Java 中的单例模式,看完这一篇就够了

单例模式是最常见的一个模式,在Java中单例模式被大量的使用。这同样也是我在面试时最喜欢提到的一个面试问题,然后在面试者回答后可以进一步挖掘其细节,这不仅检查了关于单例模式的相关知识,同时也检查了面试者的编码水平、多线程方面的知识,这些在实际的工作中非常重要。

在这个简单的Java面试教程中,我列举了一些Java面试过程中关于单例模式的常会被提到的问题。关于这些面试问题,我没有提供答案,因为你通过百度搜索很容易找到这些答案。

那么问题就从什么是单例模式?你之前用过单例模式吗?
开始

  定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

   类型:创建类模式


类图知识点:

1.类图分为三部分,依次是类名、属性、方法

2.以<<开头和以>>结尾的为Stereotype

3.修饰符+代表public,-代表private,#代表protected,什么都没有代表包可见。

4.带下划线的属性或方法代表是静态的。

5.对类图中对象的关系不熟悉的朋友可以参考文章:设计模式中类的关系

单例模式应该是23种设计模式中最简单的一种模式了。它有以下几个要素:

  • 私有的构造方法
  • 指向自己实例的私有静态引用
  • 以自己实例为返回值的静态的公有的方法

  单例模式根据实例化对象时机的不同分为两种:一种是饿汉式单例,一种是懒汉式单例。饿汉式单例在单例类被加载时候,就实例化一个对象交给自己的引用;而懒汉式在调用取得实例方法的时候才会实例化对象。代码如下:


   Eager mode:

    class Singleton{  
        private Singleton(){}  
        private static final Singleton singleton = new Singleton();  
        public static Singleton getInstance(){return singleton;}  
    }  
  Lazy mode:

 

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


1) 哪些类是单例模式的候选类?在Java中哪些类会成为单例?

  (1) 系统资源,如文件路径,数据库链接,系统常量等

  (2)全局状态化类,类似AutomicInteger的使用

 

单例模式的优点:

  • 在内存中只有一个对象,节省内存空间。
  • 避免频繁的创建销毁对象,可以提高性能。
  • 避免对共享资源的多重占用。
  • 可以全局访问。

适用场景:由于单例模式的以上优点,所以是编程中用的比较多的一种设计模式。我总结了一下我所知道的适合使用单例模式的场景:

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。

  这里将检查面试者是否有对使用单例模式有足够的使用经验。他是否熟悉单例模式的优点和缺点。

2)你能在Java中编写单例里的getInstance()的代码?

很多面试者都在这里失败。然而如果不能编写出这个代码,那么后续的很多问题都不能被提及。

   

  (1)静态成员直接初始化,或者在静态代码块初始化都可以

    class Singleton{  
        private Singleton(){}  
        private static Singleton singleton ;  
        public static synchronized Singleton getInstance(){  
            if(singleton==null)  
                singleton = new Singleton();  
            return singleton;         
        }     
    }  
  该实现只要在一个ClassLoad下就会提供一个对象的单例。但是美中不足的是,不管该资源是否被请求,它都会创建一个对象,占用jvm内存。饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。

从lazy initialization思想出发,出现了下2的写法

  (2) 根据lazy initialization思想,使用到时才初始化。

    class Singleton{  
        private Singleton(){}  
        private static Singleton singleton ;  
        public static synchronized Singleton getInstance(){  
            if(singleton==null)  
                singleton = new Singleton();  
            return singleton;         
        }     
    }  
  该实现方法加了同步锁,可以有效防止多线程在执行getInstance方法得到2个对象。

缺点:只有在第一次调用的时候,才会出现生成2个对象,才必须要求同步。而一旦singleton 不为null,系统依旧花费同步锁开销,有点得不偿失。

因此再改进出现写法3


    class Singleton{  
        private Singleton(){}  
        private static Singleton singleton ;  
        public static Singleton getInstance(){  
            if(singleton==null)//1  
                synchronized(Singleton.class){//2  
                    singleton = new Singleton();//3  
                }  
            return singleton;         
        }     
    }  

这种写法减少了锁开销,但是在如下情况,却创建了2个对象:

a:线程1执行到1挂起,线程1认为singleton为null

b:线程2执行到1挂起,线程2认为singleton为null

c:线程1被唤醒执行synchronized块代码,走完创建了一个对象

d:线程2被唤醒执行synchronized块代码,走完创建了另一个对象

所以看出这种写法,并不完美。  

(4)为了解决3存在的问题,引入双重检查锁定

 
    public static Singleton getInstance(){  
            if(singleton==null)//1  
                synchronized(Singleton.class){//2  
                    if(singleton==null)//3  
                        singleton = new Singleton();//4  
                }  
            return singleton;         
        } 

      在同步锁代码块内部,再判断一次对象是否为null,为null才创建对象。这种写法已经接近完美:

a:线程1执行到1,已经进入synchronized的时候,线程挂起,线程1占有Singleton.class资源锁;

b:线程2执行到1,当它准备synchronized块时,因为Singleton.class被占用,线程2阻塞;

c:线程1被唤醒,判断出对象为null,执行完创建一个对象

d:线程2被唤醒,判断出对象不为null,不执行创建语句

      如此分析,发现似乎没问题。

      但是实际上并不能保证它在单处理器或多处理器上正确运行;

      问题就出现在singleton = new Singleton()这一行代码。它可以简单的分成如下三个步骤:

      

mem= singleton();//1
instance = mem;//2
ctorSingleton(instance);//3

  这行代码先在内存开辟空间,赋给singleton的引用,然后执行new 初始化数据,但是注意初始化是要消耗时间。如果此时线程3在执行步骤1的时候,发现singleton 为非null,就直接返回,那么线程3返回的其实是一个没构造完成的对象。

      我们期望1,2,3 按照反序执行,但是实际jvm内存模型,并没有明确的有序指定。

      这归咎于java的平台的内存模型允许“无序写入”。

 (5) 在4的基础上引入volatile

代码如下:

    class Singleton{  
        private Singleton(){}  
        private static volatile Singleton singleton ;  
        public static Singleton getInstance(){  
            if(singleton==null)//1  
                synchronized(Singleton.class){//2  
                    if(singleton==null)//3  
                        singleton = new Singleton();  
                }  
            return singleton;         
        }     
    }  

    Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。

   这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

  根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全呢?

  (6) Lazy initialization holder class模式

  这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
  1.相应的基础知识
   什么是类级内部类?

  简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。

  •   类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
  •   类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
  •   类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

     

  多线程缺省同步锁的知识

  大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

  1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
  2.访问final字段时
  3.在创建线程之前创建对象时
  4.线程可以看见它将要处理的对象时
  2.解决方案的思路

  要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。比如前面的饿汉式实现方式。但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。

  如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。

  示例代码如下:

  

public class Singleton {
    
    private Singleton(){}
    /**
     *    类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
     *    没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
     */
    private static class SingletonHolder{
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static Singleton instance = new Singleton();
    }
    
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}
 
   (6) 单例和枚举

   按照《高效Java 第二版》中的说法:单元素的枚举类型已经成为实现Singleton的最佳方法。用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。
   

public enum Singleton {
    /**
     * 定义一个枚举的元素,它就代表了Singleton的一个实例。
     */
    
    uniqueInstance;
    
    /**
     * 单例可以有自己的操作
     */
    public void singletonOperation(){
        //功能处理
    }
}
   使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

3)在getInstance()方法上同步有优势还是仅同步必要的块更优优势?你更喜欢哪个方式?

这确实是一个非常好的问题,我几乎每次都会提该问题,用于检查面试者是否会考虑由于锁定带来的性能开销。因为锁定仅仅在创建实例时才有意义,然后其他时候实例仅仅是只读访问的,因此只同步必要的块的性能更优,并且是更好的选择。

  缺点:只有在第一次调用的时候,才会出现生成2个对象,才必须要求同步。而一旦singleton 不为null,系统依旧花费同步锁开销,有点得不偿失。



4)什么是单例模式的延迟加载或早期加载?你如何实现它?

这是和Java中类加载的载入和性能开销的理解的又一个非常好的问题。我面试过的大部分面试者对此并不熟悉,但是最好理解这个概念。

5) Java平台中的单例模式的实例有哪些?

这是个完全开放的问题,如果你了解JDK中的单例类,请共享给我。

   java.lang.Runtime;

6) 单例模式的两次检查锁是什么?


   可以使用“双重检查加锁(double checked locking)”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?

  所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

  “双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它在某些情况下比synchronized的开销更小

  注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。



7)你如何阻止使用clone()方法创建单例实例的另一个实例?

该类型问题有时候会通过如何破坏单例或什么时候Java中的单例模式不是单例来被问及。

在JAVA里要注意的是,所有的类都默认的继承自Object,所以都有一个clone方法。为保证只有一个实例,要把这个口堵上。有两个方面,一个是单例类一定要是final的,这样用户就不能继承它了。另外,如果单例类是继承于其它类的,还要override它的clone方法,让它抛出异常。


8)如果阻止通过使用反射来创建单例类的另一个实例?

开放的问题。在我的理解中,从构造方法中抛出异常可能是一个选项。

  通过反射创建单例类的另一个实例:

  如果借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器,反射攻击:   

public final class HelloWorld
{
private static HelloWorld instance = null;
 
private HelloWorld()
{
}
 
public static HelloWorld getInstance()
{
if (instance == null)
{
instance = new HelloWorld();
}
return instance;
}
 
public void sayHello()
{
System.out.println("hello world!!");
}
 
public static void sayHello2()
{
System.out.println("hello world 222 !!");
}
 
static class Test
{
public static void main(String[] args) throws Exception
{
try
{
Class class1 = Class.forName("HelloWorld");
Constructor[] constructors = class1.getDeclaredConstructors();
AccessibleObject.setAccessible(constructors, true);
for (Constructor con : constructors)
{
if (con.isAccessible())
{
Object classObject = con.newInstance();
Method method = class1.getMethod("sayHello");
method.invoke(classObject);
}
}
 
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}


评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值