单例设计模式示例

常见的两种单例设计模式,一种饿汉式,也就是类加载的时候就会初始化,一种是懒汉式,需要使用到单例对象的时候去实例化,其中懒汉的设计模式存在着线程安全的问题,下面将介绍单例模式的多种实现方式,以及最好的实现方式。

1、饿汉式的单例模式示例

方式一:直接初始化

public class Single {
  private static final Single s = new Single();
  private Single(){}
  public static Single getInstance(){
    return s;
  }
}

注意:这种方式可以,但是会来类加载的时候就会初始化Single实例,即使不会用到,也会被加载,会有少许的内存浪费。所以不是最优的。

方式二:使用静态代码块进行初始化

public class Single {
  private Single() {}
  private static Single s = null;  // 第1行

  static {
    s = new Single();
  }

  public static Single getInstance() {
    return s;
  }

  public static void main(String[] args) {
    System.out.println(getInstance().hashCode());
    System.out.println(getInstance().hashCode());
  }
}

注意:如果写静态域和声明静态变量时,顺序对结果会有影响,以上第1行如果放到static块下面,运行之后会抛出空指针异常,静态代码块是按照顺序执行的,和变通的变量及方法不同。这种方式和上述的方式一样,即使不适用也会被初始化。

方式三:通过静态内部类

public class Single {

  private Single() {}

  public static Single getInstance() {
    return InnerClass.s;
  }

  private static class InnerClass {
    private static final Single s = new Single();
  }

  public static void main(String[] args) {
    System.out.println(getInstance());
  }
}

注意:这种方式在类加载的时候不会自动初始化Single类,只有在调用的时候才会去实例化一次,相对于上面的两种略微好一点。

方式四:通过枚举方式

public class Single {
  private Single() {}
  public static Single getInstance() {
    return Singleton.INSTANCE.getInstance();
  }
  private enum Singleton {
    INSTANCE;
    private Single s;

    // JVM保证这个方法绝对只调用一次
    Singleton() {
      s = new Single();
    }
    public Single getInstance() {
      return s;
    }
  }
}

注意:这种方式最安全,也最优雅,推荐使用这种方式实现饿汉式的单例模式。

2、懒汉式的单例模式示例

方式一:直接判断对象是否创建,然后创建

@NotThreadSafe
public class Single{
  // 私有构造函数
  private Single() {}

  // 单例对象
  private static Single s = null;

  // 静态的工厂方法
  public static Single getInstance() {
    if (s == null) {
      s = new Single();
    }
    return s;
  }
}

注意:这种实现是线程不安全的,多线程环境下,可能出现多个Single对象。

方式二:通过同步方法保证线程安全

@ThreadSafe
public class Single {
  private Single() {}

  private static Single s = null;

  // 使用synchronized保证线程的安全
  public synchronized static Single getInstance() {
    if (null == s) {
      s = new Single();
    }
    return s;
  }
}

注意:通过在方法上添加synchronized同步关键字保证线程安全,为方法级别的同步,锁粒度较大。

方式三:通过同步代码块保证线程安全

@NotThreadSafe
public class Single {
  // 构造方法私有化
  private Single() {}
  // 单例对象
  private static Single s = null;
  // 静态的工厂方法
  public static Single getInstance() {
    if(null == s) {
      synchronized (Single.class) {
        if(null == s) {
          s = new Single(); // 第9行
        }
      }
    }
    return s;
  }
}

注意:上述是通过在代码块上使用同步保证了线程的安全。但是还有一个问题,就是第9行,由于JAVA虚拟机在对字节码进行优化时,会进行指令的重排序,上述第9行的对象实例化,正常情况下相当于如下的三个过程:

第一步:memory = allocate() 分配对象的内存空间;
第二步:ctorInstance() 初始化对象;
第三步:instance = memory 设置instance指向刚分配的内存;

 

如果第二步和第三步发生了指令重排序。即:第一步执行完成,然后执行赋值操作,后执行初始化对象操作。可能就会发生使用单例对象的时候,这个对象还没初始化,就会出现意想不到的错误。

方式四:对方式二进行加强,使用volatile关键字

@ThreadSafe
public class Single {
  // 构造方法私有化
  private Single() {}

  // 单例对象(使用volatile阻止指令重排导致的线程安全问题)
  private static volatile Single s = null;

  // 静态的工厂方法
  // (volatile + 双重检测) --> 防止指令重排
  public static Single getInstance() {
    if(null == s) {
      synchronized (Single.class) {
        if(null == s) {
          s = new Single();
        }
      }
    }
    return s;
  }
}

注意:上述使用了volatile关键字避免了JVM指令对其进行重排序,保证了创建出的对象为安全的对象。如果需要使用懒汉单例设计模式,推荐使用这种方法。

以上,对创建单例对象的方式做了总结及比较,详细介绍了每种方式的特点和问题。

注:文章属作者原创,如果转发,请务必标注出处:https://www.jinnianshizhunian.vip