【问题标题】:Both Singleton implementations equally Multi Threading proof?两个单例实现同样多线程证明?
【发布时间】:2015-07-16 13:50:57
【问题描述】:

在下面的代码 sn-ps 中有两个 Singleton 的实现。 它们都是在 Java 多线程环境中实现单例的正确方法吗?

如果是这样,那是否意味着第二种实现方式完全没用了,无论何时你想实现一个Singleton,就用枚举方式?

我最喜欢的写单例的方式:

public enum Singleton {
  INSTANCE();
}

另一种写单例的方式(显式同步):

public class Singleton {

  private static Singleton instance = null;
  private Singleton() {}

  public static synchronized Singleton getInstance() {
    if (instanz == null) {
      instanz = new Singleton();
    }
    return instanz;
  }

【问题讨论】:

    标签: java multithreading singleton


    【解决方案1】:

    一些程序员讨论如何创建单例的原因是他们希望支持惰性创建但担心后续调用访问器方法的性能。

    所以你的类使用synchronized 实现了惰性创建方面,但后续调用可能会受到synchronized 的不必要开销的影响。为了克服这种假设的开销,发明了臭名昭著的双重检查锁定,这引发了更多关于这个实际上完全无关的“问题”的讨论:

    如果你想将惰性创建与单例结合起来,你可以使用:

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

    整个构造是惰性的,因为类初始化不会在第一个线程调用 getInstance() 之前发生,并且它是线程安全的,因为 JVM 保证类初始化本身是线程安全的。这是最有效的解决方案,因为后续调用只会读取 final 字段,而无需同步。

    这正是使用时发生的情况

    public enum Singleton {
      INSTANCE;
    }
    

    它使您免于声明 private 构造函数,因为编译器会为您这样做。该字段隐式为static final。一个区别是字段本身是公开的,不需要访问器方法。

    另外,enums 本质上支持序列化。存储常量时,不会存储实例数据,只存储类型和常量名称。并且在反序列化时,将查找当前运行时实例。所以如果你需要序列化支持,enum 应该是首选,否则没有太大区别(使用enum 可以节省一些输入)。

    但是

    • 您的应用程序中多久需要一次单例? (如果您说“经常”,您应该重新考虑您的软件设计)
    • 单例的访问器方法的性能多久与您的应用程序相关?
    • 单例实例创建的惰性有多重要?
    • 如果您需要单例,您真正需要对单例属性进行“铁定”保证的频率是多少?即,如果有人设法使用肮脏的反射技巧创建了第二个实例,那么可能出现的问题就是他的问题,不是吗?

    简单地说,单例设计模式似乎被严重高估了,关于如何有效实施它的讨论甚至更多……

    【讨论】:

    • 我认为你的回答很好,但不幸的是最终提供了太多不相关的信息...... Singleton是否应该被视为“反模式”是完全不同的杯子茶。特别是您最后一个“to-sum-it-all-up”短语与我的问题完全无关。无论如何,在极少数情况下,单例模式是实现特定目标的唯一且最佳方式。
    • 我并不是说你不需要单例模式。我是说它的使用非常少见,例如,您不需要关心使用 synchronized 对性能的影响。因此,您可以毫不犹豫地使用问题的第二个代码示例。我还说,例如,您不需要比较使用enum 或普通单例类所做的保证有多难。在实践中,这些方法之间的差异是无关紧要的。
    【解决方案2】:

    你们两种方法看起来都不错。但是,对于创建 Singleton,我将建议以下方法,该方法是 100% 线程安全且没有同步问题,因为它利用了 Java 的类加载机制。

    P.S.:感谢 @james 的触发器。

    懒惰的单身人士:
    像这个类 Provider 只会被加载一次,因此 Network 类的实例只会存在一个,并且没有两个线程可以在同一个 JVM 中创建 2 个实例,因为类不会被加载两次。

    优点:

    • 仅在需要时创建实例。
    • 调用getInstance()时无需承担同步成本,当有getInstance()同步时会发生这种情况。
    • 类中可以有其他静态方法,不需要创建实例就可以调用,只有在需要实例方法调用时才创建实例。这是惰性单例的一个很好的优势。

      public class Network {
      private Network(){
      
      }
      
      private static class Provider {
          static final Network INSTANCE = new Network();
      }
      
      public static Network getInstance() {
          return Provider.INSTANCE;
      }
      //More code...
      

      }

    渴望单身:

    private static Network INSTANCE = null;
    
    private Network(){
    }
    
    static {
        INSTANCE = new Network();
    }
    
    public static Network getInstance() {
        return INSTANCE;
    }
    


    最后:你说得对,你指定的第二种方法确实是最后选择,因为它必须承担每次 2 个线程试图获取实例时的同步成本单身人士。

    现在,它的 ENUM 单例与懒惰单例的博弈。我为您提供了一些关于为什么人们不喜欢 ENUM 作为单例的链接,以及 ENUM 单例与基于类加载的惰性单例的可伸缩性。

    这两种方法都可以无缝地工作,所以它的选择问题,因此真正的答案是“这取决于!!!” :)

    【讨论】:

    • 那么除了需要更多代码来实现之外,还有什么比枚举方式更好/不同?
    • 类加载方法非常简单,易于开发人员理解,并且可靠..它保证工作并且会工作,直到JVM开始加载相同的类2次,这真的不是可能..
    • 枚举方法通过利用 Java 的保证来实现单例,即任何枚举值在 Java 程序中只实例化一次。现在,如果您想比较哪个在未来版本中更不可能发生变化 - 枚举值仅实例化一次或类仅加载一次? (但是两者都不太可能,但我想说明一下可能性)所以,我认为类加载更具可扩展性。
    • @Jan Hagrawal 的示例有一个名称。 en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom 这是一个优雅的解决方案,发明于枚举尚未引入 Java 的时候,Java 内存模型存在一些漏洞,这意味着双重检查锁定不一定有效。
    【解决方案3】:

    第一个解决方案适合您创建单例,因为您想对某事物的单个实例进行建模。在此解决方案中,如果在创建实例(如找不到文件)期间发生异常,您将获得 classnotfound 异常。

    第二种解决方案适用于创建单例,因为创建对象需要很长时间,并且涉及对文件系统或网络的访问。

    【讨论】:

      【解决方案4】:

      两者都是线程安全的,但第一个使用枚举可以避免不必要的锁定,并且在大多数情况下更可取。

      【讨论】:

      • 您能否定义不宜使用枚举的情况?
      • 在极少数情况下,您不希望在首次使用类时加载单例,您可以使用静态单例持有者模式(也称为按需初始化)。但这可能仅在需要单例的 1% 情况下是必需的。在其他 99% 的情况下,枚举完全没问题。
      猜你喜欢
      • 2012-11-03
      • 2013-06-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多