vlambda博客
学习文章列表

如何手写一个单例模式

什么是单例模式

单例模式,指的是在程序运行过程中,类只有一个对象实例。

为了实现单例,就不能允许程序员在使用这个类时,可以用new关键字来创建一个该类的对象。为了实现这一目的,我们可以将类的构造器设为私有,并提供一个方法来返回唯一的一个实例对象。

那么,这里引出了一个问题:该类的实例对象是在什么时候创建的呢?

一种思路是:在该类加载的时候就创建该类的实例对象,这种方式叫作饿汉式单例

饿汉式单例可以的具体实现如下所示:

public class Singleton{
private static Singleton instance = new Singleton();

private Singleton(){}

public static Singleton getInstance(){
return instance;
}
}

另一种思路是:只有在第一次使用该类的实例化对象的时候,才去创建该类的实例对象,这种方式叫作懒汉式单例

懒汉式单例的具体实现方式如下:

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

有什么问题

以上代码在单线程的时候并不会产生问题,但是在多线程高并发情况下,类Singleton可能会有多个实例对象,这就破坏了单例。

我们用具体代码来演示这一种情况:

在这里,我们使用20个线程,每个线程都会调用类Singleton的getInstance()方法,打印获取的对象的哈希码。按照单例模式的定义,由于类Singleton只有一个实例化的对象,那么按理说每一次调用getInstance()方法得到的是同一个对象,打印的哈希码是一致的。

public class Test {
   public static void main(String[] args) {
       int len = 20;
       Thread[] tn = new Thread[len];
       for(int i = 0; i < len; i++){
           tn[i] = new Thread(()->{
               System.out.println(Singleton.getInstance());
          });
           tn[i].start();
      }
  }
}

然而,我们发现,上述代码的运行结果出现了不同的哈希码,说明创建出了不同的对象,这不符合单例模式的特点。

 

单例模式被破坏的原因是:当启动多线程后,假设线程A进入if(instance==null){...}之后,判断instance的值为null,之后线程A将会执行创建类Singleton的对象的语句。在线程A将要创建类Singleton的对象的时候,线程B也进入到if(instance == null){...},此时由于线程A还没创建类Singleton的对象,线程B判断instance的值为null,也执行了创建类Singleton的对象的语句。这样子,就会造成类Singleton有多个实例对象。

如何解决线程安全的问题

  1. 声明synchronized关键字

由于多线程下调用getInstance()方法会产生线程安全的问题,因此通过synchronized关键字加锁,就可以解决这个问题。这个方法的效率比较低

 synchronized public static Singleton getInstance(){
       if(instance == null){
           instance = new Singleton();
      }
       return instance;
  }
  1. 对代码块用synchronized关键字修饰,这个效率比用synchronized关键字修饰整个方法效率高一点

public static Singleton getInstance(){
   synchronized(Singleton.class){
       if(instance == null){
           instance = new Singleton();
      }
  }
   return instance;
}
  1. 使用DCL双检查锁机制

public class Singleton{
   private volatile static Singleton instance;
   
   private Singleton(){}
   
   public static Singleton getInstance(){
       if(instance == null){
           synchronized(Singleton.class){
               if(instance == null){
                   instance = new Singleton();
              }
          }
      }
       return instance;
  }
}
  1. 使用静态内部类实现单例

public class Singleton{
   private Singleton(){}
   private static class SingletonHandler{
       private static Singleton instance = new Singleton();
  }
   
   public static Singleton getInstance(){
       return SingletonHandler.instance;
  }
}
   
  1. 序列化和反序列化单例模式的实现

使用静态内部类可以达到线程安全,但是如果遇到序列化对象时候,使用默认的方式运行得到的结果还是多例的

问题:当一个单例序列化后再反序列化,序列化前对象的hashCode和反序列化后对象的hashCode不一样

解决办法:在反序列化中使用readResolve()方法

readResolve()需要定义在单例类中,但是不需要显式地调用,在反序列化过程中,JVM会自动调用;

注意,该方法是私有的

public class Singleton implements Serializable{
   private static final long serialVersionUID = 888L;
   //内部类方式
   private static class SingletonHandler{
       private static Singleton instance = new Singleton();
  }
   private Singleton(){}
   public static Singleton getInstance(){
       return SingletonHandler.instance;
  }
   private Object readResolve() throws ObjectStreamException{
       return SingletonHandler.instance;
  }
}