【问题标题】:Initialize static variable with non-static member variable exactly once只用非静态成员变量初始化静态变量一次
【发布时间】:2020-12-09 23:47:56
【问题描述】:

我想使用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   
    }

}

【问题讨论】:

  • 请注意,您使用 run() 方法的方法已损坏。正确的做法是将 doSomething 放在 try/finally 块中,将 decrementAndGet 放在 finallyt 块中。否则,如果您的线程以异常方式结束,则 GAUGE 永远不会递减。
  • 好的,谢谢。为了简化,我省略了 try/finally 块。

标签: java spring synchronization prometheus micrometer


【解决方案1】:

您当前的同步调用没有按照您的想法进行。在“this”上同步不会阻止 RunInAThread 的两个实例同时被实例化,并且都将 GAUGE 检测为 null 并设置它。

从您的代码示例中,不清楚为什么 GAUGE 必须是静态的。它来自注册表,尚不清楚是否可以保证两个不同的注册表对象将返回相同的 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);

我认为关键是你试图解决这个问题,低于你应该达到的水平。

【讨论】:

  • 从增加 GAUGE 的 run() 方法的额外提供的上下文中,很明显 GAUGE 应该是静态的,否则它所做的就是移动到 1,然后回到0,反映线程的运行状态,这似乎毫无意义。
  • @rzwitserloot 上面所说的
【解决方案2】:

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 也建立了先于,从而保证 VM 将确保我们对 GAUGE 变量的视图被更新;因此,没有必要让 GAUGE 变得不稳定。
  2. 如果我们很幸运并且 GAUGE 变量由于任何原因已更新,那么我们永远不会同步。
  3. 由于第二次 nullcheck,GAUGE 仍然不可能是 [A] null 或 [B] 一次的返回值以外的任何值,我们称之为注册表。
  4. 这称为“双重检查锁定”。这两个空检查至关重要。

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

【讨论】:

  • 好的,感谢您的所有意见。听起来采用同步的 getInstance 方法是更好的选择。我对简单仪表所需的所有样板代码感到有些困惑,因为我认为这是一个常见的用例,但这是另一回事;-)
猜你喜欢
  • 1970-01-01
  • 2021-10-23
  • 1970-01-01
  • 2011-10-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多