面试之手写代码

单例模式

单例模式定义:确保一个类只有一个实例,并且提供该实例的全局访问点。
优点:有些实例,全局只需要一个就可以,使用单例模式可以避免一个全局使用的类,频繁的创建与销毁,浪费系统资源。

单例模式的使用场景

  1. 有频繁实例化然后销毁的情况,也就是频繁new对象,可以考虑使用单例模式。
  2. 创建对象时候耗时过多或者消耗资源过多,但是又常用到的对象。
  3. 频繁访问IO资源的对象,例如数据库连接池或访问本地文件。
  4. 要求生产唯一的序列号。
  5. web中的计数器不需要每次刷新都在数据库里加一次,用单例先缓存起来。
  6. 应用程序日志应用
  7. 使用线程池之类的控制资源时,使用单例模式,可以方便资源之间的通信。

单例模式的设计要素!!!

为了实现单例模式一个类只有一个实例的限制,需要考虑以下几点要素:

  1. 一个私有构造函数(确保只能单例类自己创建实例
  2. 一个私有静态变量(确保只有一个实例
  3. 一个公有静态函数(给使用者提供调用方法
    于是编写函数时候可以这样理解,单例类的构造方法不让其他人修改和使用;并且单例类自己只创建一个实例,这个实例,其他人也无法修改和直接使用;然后单例类提供一个调用方法,想用这个实例,只能调用。这样就确保了全局只创建了一次实例!!!

单例模式6种实现方式

(一)懒汉式(线程不安全)

懒汉,顾名思义,就是实例在用到的时候才去创建。
单例模式的懒汉式体现了延迟加载的思想(一开始不要加载资源或者数据,等到使用的时候才加载)。
实现:

public class Singleton{
    //创建一个私有静态变量
    private static Singleton uniqueInstance;
    
    //创建一个私有构造函数防止直接new
    private Singleton(){};
    
    //一个公有的静态函数
    public static Singleton getUniqueInstance(){
        if(uniqueInstance == null){
           uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

说明: 懒汉式先不创建实例,当第一次被调用时候(判断uniqueInstance是否为空),再创建实例,所以被称为懒汉式。
优点:延迟实例化,如果不需要使用该类,就不会实例化,节约了系统资源。
缺点:线程不安全。

(二)饿汉式(线程安全)

实现

public class Singleton{
    //静态初始化器(static initializer)种创建实例,保证线程安全
    private static Singleton uniqueInstance = new Singleton();
    
    private Singleton(){};
    public static Singleton getUniqueInstance(){
       return uniqueInstance;
    }
}

说明:先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用。
优点:线程安全
缺点:直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费。

(三)懒汉式(线程安全)

实现

public class Singleton{
    private static Singleton uniqueInstance;
    private Singleton(){}

    //解决了线程不安全问题,但是效率太低了
    //每个线程想获得类的实例的时候,都需要同步方法
    public static synchronized Singleton getUniqueInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
     }
} 

说明:在get方法上加了一把锁。多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。
优点:节约资源,线程安全
缺点:虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方法,会使线程阻塞,等待时间过长。

(四)双重检查锁实现(线程安全)

实现

public class Singleton{
    //volatile关键词确保多线程正确处理singleton
    private static volatile Singleton uniqueInstance;
    private Singleton(){}
    
    private static Singleton getUniqueInstance(){
       if(uniqueInstance == null){
          synchronized(Singleton.class){
             if(uniqueInstance == null){
                uniqueInstance = new Singleton();
             }
           }
        }
        return uniqueInstance;
    }
}

说明

  • volatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

  • 双重检查数相当于是改进了 线程安全的懒汉式。线程安全的懒汉式 的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。

  • 指令重排:指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。比如Java中创建一个对象,往往包含三个过程。对于uniqueInstance = new Singleton(),这不是一个原子操作,在 JVM 中包含如下三个过程:

    1. 给uniqueInstance分类内存空间。
    2. 初始化uniqueInstance,调用Singleton的构造函数来初始化成员变量,形成实例。
    3. 将uniqueInstance对象指向分配的内存空间(执行完这一步uniqueInstance才是非null)。

    正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。 单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。 例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。

    解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。

优点:延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。
缺点:volatile 关键字,对性能也有一些影响。

(五)静态内部类实现(线程安全)

实现

public class Singleton{
   private Singleton(){}

   private static class SingletonHolder{
      private static final Singelton INSTANCE = new Singleton();
   }

   public static Single getUniqueInstance(){
      return SingletonHolder.INSTANCE;
   }
}

说明:首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE; 触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次(类的静态属性只会在第一次加载类的时候初始化,所以,JVM帮我们保证了线程的安全性,在类初始化时,其他线程无法进入)。
优点:延迟实例化,节约了资源;线程安全,性能提高。

(六)枚举类实现

实现

public enum Singleton{
   INSTANCE;
    
   //添加自己需要的操作
   public void method(){
   }
}

说明:默认枚举实例的创建时线程安全的,并且再任何情况下都是单例。借助JDK5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。
优点:写法简单,线程安全,天然防止反射和反序列化调用。

另:如何反序列化

反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。 我们要防止反序列化,避免得到多个实例。枚举类天然防止反序列化。 其他单例模式 可以通过 重写 readResolve() 方法,从而防止反序列化,使实例唯一重写。

public Object readResolve() throws ObjectStreamException{
    return singleton;
}

你可能感兴趣的:(我的面试准备,面试,单例模式,java)