【问题标题】:Why are there two instances created of a singleton Java class?为什么要创建一个单例 Java 类的两个实例?
【发布时间】:2021-03-23 20:53:29
【问题描述】:

我已经浏览了互联网上的一些相关主题,例如 this 和这里的问题,例如 thisthisthis,但我无处可去。这是我的简化代码:

MainActivity

package com.test.staticvariables;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Test1 test1 = Test1.getInstance();
    // do something
    Test2.printTest1Instances();
  }
}

测试1

package com.test.staticvariables;

public class Test1 {

  private static Test1 test1;

  static {
    System.out.println("Initializing Test1, loader: " + " " + Test1.class.getClassLoader());
  }

  static synchronized Test1 getInstance() {
    if (test1 == null) {
      test1 = new Test1();
    }
    return test1;
  }

  private Test1() {}
}

测试2

package com.test.staticvariables;

public class Test2 {

  private static final Test1 test1;
  // private static final Test1 test1 = Test1.getInstance();
  // private static final Test1 test1 = getTest1Instance();

  static {
    System.out.println("Initializing Test2, loader: " + " " + Test2.class.getClassLoader());
    test1 = Test1.getInstance();
  }

  static Test1 getTest1Instance() {
    return Test1.getInstance();
  }

  private Test2() {}

  static void printTest1Instances() {
    System.out.println("Test1 class variable: " + test1);
    System.out.println("Test1 instance variable: " + Test1.getInstance());
  }
}

结果:

Initializing Test1, loader: dalvik.system.PathClassLoader[DexPathList...]
Initializing Test2, loader: dalvik.system.PathClassLoader[DexPathList...]
Test1 class variable: com.test.staticvariables.Test1@2a7bfa4
Test1 instance variable: com.test.staticvariables.Test1@7e2a464

为什么要创建两个 Test1 类的实例(2a7bfa47e2a464)?

请注意Test2 只包含静态方法,它没有被实例化。

应用程序在单个本地进程中运行,因此类应该由同一个类加载器加载(如果我理解正确的话)。

声明和初始化(在静态方法或静态初始化块内部或外部)保存其他类实例的最终静态变量是错误/不好的做法吗?还是在某些情况下是错误的?

【问题讨论】:

  • 我不了解你,但对我来说没问题,唯一的问题是,我会修改你的 getInstance() 方法。
  • 这可能是内存可见性问题。尝试使 Test1.test1 不稳定。
  • 您是否涉及多个类加载器?如果是这样,你应该在你的问题中解释这一点。

标签: java android class static-initialization


【解决方案1】:

这似乎是一个非常有趣的问题。我已经运行了相同的项目,但直接从 public static void main 运行,它返回了预期的良好结果。没有损坏。

所以它必须与 android 以及如何在您的项目中加载类有关。

【讨论】:

    【解决方案2】:

    这里没问题

    我用纯 Java 而不是 Android 编写了类似的代码。我选择了Gilligan’s Island 的主题,以便比Test1/2 更容易混淆。

    似乎运行正常。

    首先是单例类,Gilligan

    package work.basil.example;
    
    import java.time.Instant;
    
    public class Gilligan
    {
        private static Gilligan gilligan;  // Hold a singleton.
    
        static
        {
            System.out.println( "Static class loading of Gilligan, loader: " + " " + Gilligan.class.getClassLoader() + " at " + Instant.now() );
        }
    
        static synchronized Gilligan getInstance ( )
        {
            // Lazy loading of our singleton, an instance of `Gilligan`.
            if ( gilligan == null )
            {
                gilligan = new Gilligan();
            }
            return gilligan;
        }
    
        // Constructor - private
        private Gilligan ( ) {}
    }
    

    第二个类是Island,持有对Gilligan的同一个单例的静态引用。

    package work.basil.example;
    
    import java.time.Instant;
    
    public class Island
    {
        private static final Gilligan gilligan;
    
        static
        {
            System.out.println( "Static class loading of Island, loader: " + " " + Island.class.getClassLoader() + " at " + Instant.now() );
            gilligan = Gilligan.getInstance();
        }
    
        // Constructor - private
        private Island ( ) {}
    
        static void proveSingleton ( )
        {
    
            boolean isSingleton = ( Island.gilligan == Gilligan.getInstance() );
            System.out.println( "Island.gilligan: " + gilligan );
            System.out.println( "Gilligan.gilligan: " + Gilligan.getInstance() );
            System.out.println( "Gilligan is a singleton: " + isSingleton );
        }
    }
    

    最后,在名为 Solo 的类中运行该代码的应用程序。

    package work.basil.example;
    
    public class Solo
    {
        public static void main ( String[] args )
        {
            Gilligan g = Gilligan.getInstance();
            Island.proveSingleton();
        }
    }
    

    运行时:

    Static class loading of Gilligan, loader:  jdk.internal.loader.ClassLoaders$AppClassLoader@73d16e93 at 2020-12-13T00:10:13.009691Z
    Static class loading of Island, loader:  jdk.internal.loader.ClassLoaders$AppClassLoader@73d16e93 at 2020-12-13T00:10:13.028805Z
    Island.gilligan: work.basil.example.Gilligan@4c873330
    Gilligan.gilligan: work.basil.example.Gilligan@4c873330
    Gilligan is a singleton: true
    

    volatile

    正如有人评论的那样,您可能会看到缓存内存可见性问题。如果是这样,可能需要用volatile 标记你的单例变量。

    如果不熟悉该关键字和此问题,并且您正在使用线程,请在 Java Memory Model 上学习。阅读并重新阅读 Brian Goetz 等人的 Java Concurrency in Practice

    你可能会考虑以不同的方式实现你的单例。

    多个类加载器

    您的应用运行时是否涉及多个类加载器?如果是这样,请编辑您的问题以进行解释。并查看问题,Singleton class with several different classloaders

    Enum 用于单例

    我相信 Java 世界的共识是,使用简单但功能强大的 enum facility in Java 是实现单例的最佳和最安全的方式。

    package work.basil.example;
    
    enum Gilligan
    {
        INSTANCE;
    }
    

    要访问实例,请使用 Gilligan.INSTANCE 而不是调用 getter 方法。

    并且不需要在你的第二个类中存储对单例的静态引用。只需在代码中需要访问单例的任何地方使用 Gilligan.INSTANCE

    警告:为单例使用枚举不能解决运行时多个类加载器的问题。

    见:Implementing Singleton with an Enum (in Java)

    我将跳过通常的注意事项,即使用单例有时会掩盖糟糕的 OOP 设计,并可能使测试变得困难。

    【讨论】:

    • 感谢您的详细回答。我知道volatile 现象和并发,虽然不是专家。几个星期前,我花了几乎一整夜的时间来玩这个 b/c,我以为我错过了一些非常简单的东西。然后我更改了代码以在方法中实例化类,而不是使用类变量。但我无法得到这个奇怪行为的答案。至于类加载器,我对此不太确定。除了 DVM 的默认行为之外,我没有触及这个区域。我会花更多时间对此进行调查。
    猜你喜欢
    • 1970-01-01
    • 2018-08-26
    • 1970-01-01
    • 1970-01-01
    • 2016-02-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-20
    • 1970-01-01
    相关资源
    最近更新 更多