静态成员的多线程行为

问题描述

多线程在静态成员的情况下如何表现?就像在单例类的情况下一样,如果我尝试在静态块和静态方法中创建实例,我会返回实例,并且两个线程尝试同时执行 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;
    }
}

解决方法

这个具体的例子?

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

一旦您解决了这个异常处理问题,您就可以将变量设为 final,并且不再为其赋值。当你在做的时候,让整个班级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 覆盖。最好只是得出结论,一切都结束了,说出来,然后停止运行。
,

查看 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
,

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

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

然后进一步说:

初始化C的过程如下:

同步 C 的初始化锁 LC。这涉及到等待当前线程可以获取 LC

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

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

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

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


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