【问题标题】:Multithreading behaviour with Static Members静态成员的多线程行为
【发布时间】:2021-02-19 04:33:23
【问题描述】:

多线程在静态成员的情况下如何表现?就像在单例类的情况下,如果我尝试在静态块和静态方法中创建实例,我返回实例并且两个线程尝试同时执行 getInstance() ..这将如何在内部表现为静态是只加载一次

public class SingleTonUsingStaticInitialization {

    private static SingleTonUsingStaticInitialization INSTANCE = null;

    static {
        try {
            INSTANCE = new SingleTonUsingStaticInitialization();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private SingleTonUsingStaticInitialization() {
    }

    public static SingleTonUsingStaticInitialization getInstance() {
        return INSTANCE;
    }
}

【问题讨论】:

    标签: java multithreading static java-memory-model


    【解决方案1】:

    这个具体的例子?

    线程方面很好。风格明智,这是可悲的。不要写这样的 catch 块。这也意味着如果确实发生了异常(它不能在这里 - 你的构造函数是空的),你的代码会将一半信息转储到系统错误,然后继续,使用 Singleton 实例的空引用实例 - 导致其他吐出 NullPointerExceptions 的代码(因为代码一直在运行,因为您捕获了异常而不是让它发生)。如果您以这种方式处理所有异常,则单个错误将导致日志中出现数百个错误,除了第一个错误之外,所有错误都无关紧要。

    一旦你处理了这个异常处理问题,你就可以创建变量final,并且不再给它赋值null。当你在它的时候,让整个班级final。它实际上已经是(因为您只有一个私有构造函数):

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

    当 2 个线程同时调用 getInstance 时会出现这种情况的原因在于类加载器机制本身:类加载器保证任何给定的类都不会被同一个加载器多次加载,即使 2 个线程将同时需要这个(类加载器将同步/锁定以避免这种情况),并且初始化过程(静态块 - 如上例所示,这是不必要的复杂化)同样受到保护,不可能发生两次。

    这是您获得的唯一免费赠品:对于静态方法,作为一般规则,所有线程只要愿意,都可以同时运行相同的方法。在这里他们做 - 只是初始化(包括 ... = new Singleton(); 部分)只发生一次。

    注意:如果您必须做更复杂的事情,请制作辅助方法:

    public final class Singleton {
        private static Singleton INSTANCE = create();
    
        private Singleton(Data d) {
            // do stuff with 'd'
        }
    
        private static Singleton create() {
            Data d;
            try {
                d = readStuffFromDataIncludedInMyJar();
            } catch (IOException e) {
                throw new Error("states.txt is corrupted", e);
            }
            return new Singleton(d);
        }
    }
    

    这个:

    1. 保持代码简单 - 静态初始化器是一个东西,但相当奇特的 java。
    2. 使您的代码更易于测试。
    3. 这是一个内部文件;如果它丢失/损坏,那与您的一个类文件出去散步一样可能/同样有问题。此处保证有错误。这不可能发生,除非你写了一个错误或搞砸了一个构建,并且硬崩溃并有一个明确的异常告诉你究竟出了什么问题正是你想要在这种情况下发生的事情,而不是让代码在一半的状态下盲目地继续由于磁盘驱动器崩溃或其他原因,您的应用程序被 gobbledygook 覆盖。最好只是结束一切都无聊,这样说,然后停止运行。

    【讨论】:

    • 亮点。但我对第二个代码 sn-p 感到困惑。我看不到create 如何返回Singleton 类的必要实例。我错过了什么吗?你的评论说在构造函数中做复杂的事情。但是您不是已经在 create 方法中而不是而不是构造函数中做复杂的事情吗?
    • 第二个 sn-p 是不完整的存根代码。我知道这听起来很苛刻,但在开始使用多线程之前,您应该熟悉这种代码。多线程是一个非常难的概念,如果你不了解编程的基础知识是不可能学习的。
    • 是的,很抱歉,代码 sn-p 有点匆忙。当我在那里时,我已经修复了它并修复了一些错别字。
    • 我会使用ExceptionInInitializerError,而不是不明确的Error。无需添加额外的消息(对问题进行猜测)。链接的IOException 确实已经提供了所有信息。
    【解决方案2】:

    你是安全的,因为getInstance 会将 same 实例返回给多个线程。这是由The JLS 保证的,这是您唯一应该将理解委托给它的地方。具体来说,该章说:

    对于每个类或接口C,都有一个唯一的初始化锁LC

    然后继续说:

    那么初始化C的过程如下:

    在 C 的初始化锁 LC 上同步。这包括等待当前线程可以获取 LC

    简单来说,只有一个线程可以初始化 static 字段。期间。

    该锁的释放在静态块中的操作与使用该静态字段的任何线程之间提供了适当的happens-before 保证。这是从同一章中暗示的,来自:

    当一个实现可以确定类的初始化已经完成时,可以通过省略步骤 1 中的锁获取(并在步骤 4/5 中释放)来优化此过程,前提是,就内存模型而言,如果获得锁,所有发生前发生的排序,在执行优化时仍然存在

    或者,同样,用简单的英语,static 块中发生的任何事情都将对所有阅读线程可见。


    为此,您将拥有一个适当的工具来通过所谓的“恒定动态”删除该static 块。它的基础设施已经到位,但javac 仍然没有使用它。你可以阅读more here about it。有些项目已经在使用它——如果你有合适的 jdk,例如 jacoco 就可以了。

    【讨论】:

      【解决方案3】:

      请参阅Answer by rzwitserloot 中提出的要点。

      这里的代码与那里看到的代码相似,但适用于使用 enum 作为您的单例。许多人推荐枚举作为在 Java 中定义单例的理想方式。

      Thread-safety 得到保证,因为在另一个答案中讨论了相同的类加载器行为。当类第一次加载时,每个类加载器只加载一次枚举。

      如果您有多个线程访问此枚举定义的单个对象,则第一个到达加载此类的点的线程将导致我们的枚举对象在其构造函数运行时被实例化。其他剩余线程将阻止它们访问枚举对象的尝试,直到枚举类完成加载并且它的唯一命名枚举对象完成其构造。 JVM 自动处理所有这些争用,我们不需要进一步编码。 Java specifications 保证所有这些行为。

      package org.vaadin.example;
      
      import java.io.IOException;
      import java.nio.file.Files;
      import java.nio.file.Path;
      import java.nio.file.Paths;
      
      public enum AppContext
      {
          INSTANCE;
      
          private String wording;
      
          AppContext ( )
          {
              try
              {
                  readStuffFromDataIncludedInMyJar();
              }
              catch ( IOException e )
              {
                  throw new Error( "Failed to load from text file. Message # 7a608ddf-8c5f-4f77-a9c9-5ab852fde5b1." , e );
              }
          }
      
          private void readStuffFromDataIncludedInMyJar ( ) throws IOException
          {
              Path path = Paths.get( "/Users/basilbourque/example.txt" );
              String read = Files.readAllLines( path ).get( 0 );
              this.wording = read;
              System.out.println( "read = " + read );
          }
      
          public static void main ( String[] args )
          {
              System.out.println( AppContext.INSTANCE.toString() );
          }
      }
      

      运行时。

      read = Wazzup?
      INSTANCE
      

      【讨论】:

      • 为此使用enum 的好处是,您可以获得适合单例的序列化行为,而且您可以免费获得。但是,这是有代价的:enum 在这里毫无意义。此代码是 WTF,需要注释来解释为什么在大火中使用枚举。为了捍卫这种模式,它是众所周知的。但是,关键是:为什么要序列化一个单例?谁甚至使用 java 的序列化,甚至被其作者所鄙视?会发生,是的,但很少见,除非很重要,否则不应使用此模式。
      • 因此,我对以下陈述提出质疑如果说群众没有考虑清楚的话,对群众来说并不值得很多。这通常不是你想要制作单身人士的方式。
      • 当你说JLS 保证,你必须具体。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-10
      • 1970-01-01
      • 2011-01-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多