用非静态成员变量初始化静态变量恰好一次

Initialize static variable with non-static member variable exactly once

我想使用 https://micrometer.io/docs/concepts#_gauges 计算 运行 个 RunInAThread 实例的数量。从文档中,最好在集合上创建一个仪表(我没有),或者使用类似 AtomicInteger 的东西。

但是,我需要一个 AtomicInteger 的静态实例,并且必须使用作为成员变量的 MeterRegistry 对其进行初始化。这样做的最佳做法是什么?我不想采用标准的单例模式,因为这意味着我总是必须调用 getInstance() 来获取 AtomicInteger 实例,并且每次都必须同步。

还有比我现在做的更好的方法吗?

public class RunInAThread implements Runnable {

    private static AtomicInteger GAUGE = null;
    
    public RunInAThread(final MeterRegistry registry) {
        synchronized(this) {
            if(GAUGE==null) {
                GAUGE = registry.gauge("some_name", Collections.emptySet(), new AtomicInteger());
            }
        }     
    }

    @Override
    public void run() {
        GAUGE.incrementAndGet()
        doSomething();
        GAUGE.decrementAndGet()
    }

    private void doSomething() {   
       // processing like a boss in a while loop   
    }

}

synchronized(this) 在构造函数中是完全没用的。这意味着:避免 运行 我的大括号中包含的代码与锁定同一对象的任何其他线程同时出现。你锁定的那个物体?根据定义不是任何其他线程可能拥有的东西 - 你刚刚创建*。

听起来这个MeterRegistry概念本身就是一个单例。也许调查一下你是否可以在其他一些静态块中只初始化一次 GAUGE。但是,如果这看起来很困难或不可能,那么如果你真的想从中挤出性能,你可以使用双锁;不过,我怀疑这是否重要。同步非常快。无论如何,这在理论上应该更快:

public class RunInAThread implements Runnable {
    private static final Object GAUGE_LOCK = new Object();
    private static AtomicInteger GAUGE = null;
    
    public RunInAThread(final MeterRegistry registry) {
        if (GAUGE == null) {
            synchronized (GAUGE_LOCK) {
                if (GAUGE == null) GAUGE = registry.gauge(...);
            }
        }
    }
}

这有几件事:

  1. 这个锁其实很有用。 VM 中只有一个 GAUGE_LOCK 对象,所以如果我们确实命中那个同步,它就会工作。 synchronized 还建立了 comes-before,从而保证 VM 将确保我们对 GAUGE 变量的看法得到更新;因此,没有必要让 GAUGE 变得易变。
  2. 如果我们很幸运并且 GAUGE 变量已更新,无论出于何种原因,我们都不会同步。
  3. 由于第二次 nullcheck,GAUGE 仍然不可能是 [A] null 或 [B] 一次的 return 值,我们称之为注册表。
  4. 这叫做'double checked locking'。两个空检查是至关重要的。

*) 您可以使构造函数中的 synchronized(this) 实际上有效,但前提是您在自己的构造函数中触发线程,或者让 this 引用从构造函数中逃逸.这些都是如此荒谬的坏事,我觉得有理由假设你不会做那些愚蠢的事情。此时我们可以简化为: synchronized(this) 在构造函数中是无用的。

您当前的同步调用与您的想法不符。在 'this' 上同步不会阻止同时实例化 RunInAThread 的两个实例,并且都将 GAUGE 检测为 null 并进行设置。

从您的代码示例中,不清楚为什么 GAUGE 必须是静态的。它来自注册表,目前尚不清楚是否可以保证两个不同的注册表对象 return 相同的 AtomicInteger。现在,如果 MeterRegistry 是单例,那么一个选项就是使用单例。类似于:

private static final AtomicInteger GAUGE = MeterRegistry.getInstances().gauge(...)

编辑 想象一下代码:

MeterRegistry reg1 = new MeterRegistry(...);
RunInAThread thread1 = RunInAThread(reg1)

MeterRegistry reg2 = new MeterRegistry(...);
RunInAThread thread2 = RunInAThread(reg1)

在那种情况下,是否真的打算在两种情况下使用相同的 GAUGE?

再想一想,之前让注册表成为单例的解决方案仍然有效。或者,改为传入原子整数并将其视为成员变量。这样会更加清晰,并且不容易出现意外行为。

MeterRegistery reg = new MeterRegistry(...);
AtomicInteger gauge = reg.gauge(...);
RunInAThread thread1 = RunInAThread(gauge);
RunInAThread thread2 = RunInAThread(gauge);

我认为关键是你正在努力解决这个低于你应该达到的水平。