【问题标题】:Proper usage of Singleton getInstance method正确使用 Singleton getInstance 方法
【发布时间】:2016-11-02 05:08:54
【问题描述】:

使用静态同步 getInstance 方法的单例的正确用法是什么?为什么?

例如:

MySingleton mySingleton = MySingleton.getInstance();
.....
mySingleton.doSomething();

mySingleton 将是一个字段并在整个班级中使用。

MySingleton.getInstance().doSomething();

编辑:正确地说,我的意思是编码风格和线程安全。

【问题讨论】:

  • 理想情况下,您根本不会使用单例。必要时,我更喜欢使用enum,在这种情况下我会写MySingleton.INSTANCE.doSomething()
  • 这两个例子是相同的,尽管@PeterLawrey 推荐单例枚举是正确的。
  • 定义“正确”——您具体要问什么?除了在第一个示例中获取实例并稍后使用之外,它是相同的。
  • @AaronDavis 和 PeterLawrey 关于不使用静态单例更加正确。
  • @Jesper 在大多数情况下是正确的 :-)

标签: java singleton


【解决方案1】:

虽然我同意 Andy Turner 的回答,即尽可能避免使用单例,但为了完整起见,我将添加此内容:静态同步 getInstance 方法是过去的遗物。至少有 3 个单例模式实现不需要这样做。

a) 渴望单身

public class EagerSingleton{
  private EagerSingleton(){}
  private static final EagerSingleton INSTANCE = new EagerSingleton();
  public static EagerSingleton getInstance(){ return INSTANCE; }
}

每当您以任何方式(例如,通过声明该类型的变量)引用类 EagerSingleton 时,它都会在类加载时初始化。

b) 懒惰的单身人士

public class LazySingleton{
    private LazySingleton(){}
    private static class Holder{
        private static final LazySingleton INSTANCE = new LazySingleton();
    }
    public static LazySingleton getInstance(){ return Holder.INSTANCE; }

}

这是在调用时初始化的,在你第一次调用getInstance()方法时。类加载器现在加载持有者类并初始化 INSTANCE 字段,并保证以同步的方式执行此操作。如果您的单例设置昂贵且并非总是必需的,请使用此版本。

c) 枚举单例

public enum EnumSingleton{
    INSTANCE;

    // and if you want, you can add the method, too, but it's
    // unnecessary:
    public static EnumSingleton getInstance(){ return INSTANCE; }
}

枚举项是编译时常量,即它们的唯一性在编译时得到保证,而不仅仅是在运行时(在运行时,每个类加载器的单例都是唯一的,并且适用于所有版本)。这是最好的方法,除非您的要求是从现有类扩展。

枚举版本免费为您提供许多其他功能:

  • equals / hashCode / toString 实现开箱即用
  • 反序列化攻击防御
  • 多吨支持(只需添加另一个枚举项)

所以下次你看到有人写双重检查锁定机制或类似的东西时,告诉他们他们太复杂和技术性太强了。让类加载器为您完成工作。

对于所有 3 个版本:确保您使用的功能由接口支持,并且针对该接口编写代码,而不是实现类型。这将使您的代码可测试。

【讨论】:

    【解决方案2】:

    比使用单例更好的解决方案是使用dependency injection

    class MyClass {
      private final MySingleton mySingleton;
    
      MyClass(MySingleton mySingleton) {
        this.mySingleton = mySingleton;
      }
    
      // Use mySingleton in instance methods as required.
    }
    

    这里的优点是它是一个单例的事实并不相关 - 它可以是一个单例,但它不需要是。

    我称该字段为 mySingleton,因为这就是您在问题中所称的,但不再要求它是真正的单例:唯一的问题是 MyClass 不需要关心关于实例的生命周期。

    这打破了MyClassMySingleton 之间的静态耦合,如果您通过问题中描述的任何一种方式进行操作,就会存在这种耦合。这使得测试MyClass 变得更加容易,并具有其他优势。

    【讨论】:

    • 我同意我们不能滥用单身人士。现在,DI 需要依赖容器,而单例不需要。如果你使用依赖容器或者你想使用它,这个想法很好,但如果你不需要它并且你的项目中有 3 或 4 个经典单例,为什么要添加这个开销?测试单例比 DI 更难,但通过反射,它非常可玩。
    【解决方案3】:

    必须有人实例化一个单例。这必须只做一次。线程安全开始发挥作用了。如果有可能不同的线程调用

    SingletonClass.getInstance();
    

    几乎同时,必须像这样保护单例的实例化:

    private static volatile SingletonClass myInstance;
    
    public static SingletonClass getInstance()
    {
        if ( myInstance == null )
        {
            synchronized( SingletonClass.class)
            {
                if ( myInstance == null )
                {
                    myInstance = new SingletonClass();
                }
            }
        }
    
        return myInstance;
    }
    

    这称为“双重检查锁定”。变量必须是 volatile。

    如果 SingletonClass 的方法在线程之间共享状态(例如,您有一个由一个线程写入并由另一个线程读取的成员),则必须额外注意同步。但这很常见,与单例模式无关。

    您的问题是“XY.getInstance().doSomething()”与在自己的成员中保留引用以便以后访问它有什么区别:这只是一个(轻微的)性能损失(如果声明,返回)。但是 IMO 这是可以忽略的,因为从长远来看,Java 优化器无论如何都会内联它。另一方面:保持引用可以使代码更短、更易读,尤其是如果您经常访问单例。最后,这是一个品味问题。

    【讨论】:

      【解决方案4】:

      我认为为类中的每个单例定义一个字段会更好。因此,您只需查看其前 n 行即可轻松找出类与系统中单例的依赖关系。它有助于理解类的耦合。

      此外,为单例定义字段使测试更容易。如果您有机会将单例的方法移动到接口,您可以为单例定义一个存根类,然后可以在测试代码中注入存根单例。

      对于线程安全部分,如果你的单例是线程安全的,应该没有任何问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多