1. 概述

在这个教程中,我们将探讨执行方法仅一次的方法。这对于多种场景都非常有用,比如初始化单例实例或执行一次性设置操作。

我们将研究各种确保方法只被调用一次的技术,包括使用布尔变量和synchronized关键字、AtomicBoolean以及静态初始化块。此外,一些单元测试框架,如JUnit和TestNG,提供了可以帮助我们仅执行一次方法的注解。

2. 使用布尔变量与synchronized

我们的第一个方法是结合使用布尔标志和synchronized关键字。 让我们看看如何实现:

class SynchronizedInitializer {

    static volatile boolean isInitialized = false;
    int callCount = 0;

    synchronized void initialize() {
        if (!isInitialized) {
            initializationLogic();
            isInitialized = true;
        }
    }

    private void initializationLogic() {
        callCount++;
    }
}

在这个实现中,我们最初将isInitialized标志设为false。当initialize()方法首次被调用时,它会检查标志是否为false。如果是,就会执行一次性初始化逻辑,并将标志设为true。后续对initialize()方法的调用会发现标志已经是true,因此不会执行初始化逻辑。

同步确保每次只有一个线程可以同时执行initialize方法,防止多个线程同时执行初始化逻辑,从而避免可能出现的竞态条件。 我们需要使用volatile关键字以确保每个线程都能读取到更新后的布尔值。

我们可以用以下测试来验证只执行了一次初始化:

void givenSynchronizedInitializer_whenRepeatedlyCallingInitialize_thenCallCountIsOne() {
    SynchronizedInitializer synchronizedInitializer = new SynchronizedInitializer();
    assertEquals(0, synchronizedInitializer.callCount);

    synchronizedInitializer.initialize();
    synchronizedInitializer.initialize();
    synchronizedInitializer.initialize();

    assertEquals(1, synchronizedInitializer.callCount);
}

首先,我们创建一个SynchronizedInitializer实例,并断言callCount变量为0。在多次调用initialize()方法后,callCount会增加到1

3. 使用AtomicBoolean

另一种执行方法仅一次的方法是使用类型为AtomicBoolean的原子变量。 让我们看一个实现示例:

class AtomicBooleanInitializer {

    AtomicBoolean isInitialized = new AtomicBoolean(false);
    int callCount = 0;

    void initialize() {
        if (isInitialized.compareAndSet(false, true)) {
            initializationLogic();
        }
    }

    private void initializationLogic() {
        callCount++;
    }
}

在这个实现中,我们使用AtomicBoolean构造函数将isInitialized变量初始设为false。当我们首次调用initialize()方法时,我们调用compareAndSet()方法,传入预期的false值和新的true值。如果当前的isInitialized值为false,方法会将其设为true并返回true。后续对initialize()方法的调用会看到isInitialized变量已经为true,因此不会执行初始化逻辑。

使用AtomicBoolean确保compareAndSet()方法是原子操作,这意味着任何时候只有一个线程能修改isInitialized的值。 这样可以防止竞态条件,并确保initialize()方法是线程安全的。

我们可以用以下测试验证AtomicBooleanInitializerinitializationLogic()方法只执行了一次:

void givenAtomicBooleanInitializer_whenRepeatedlyCallingInitialize_thenCallCountIsOne() {
    AtomicBooleanInitializer atomicBooleanInitializer = new AtomicBooleanInitializer();
    assertEquals(0, atomicBooleanInitializer.callCount);

    atomicBooleanInitializer.initialize();
    atomicBooleanInitializer.initialize();
    atomicBooleanInitializer.initialize();

    assertEquals(1, atomicBooleanInitializer.callCount);
}

这个测试与之前的测试非常相似。

4. 使用静态初始化

静态初始化是另一种只执行一次方法的方法:

final class StaticInitializer {

    public static int CALL_COUNT = 0;

    static {
        initializationLogic();
    }

    private static void initializationLogic() {
        CALL_COUNT++;
    }
}

静态初始化块在类加载期间仅执行一次,无需额外的锁定。

我们可以用以下测试验证StaticInitializer的初始化方法只执行了一次:

void whenLoadingStaticInitializer_thenCallCountIsOne() {
    assertEquals(1, StaticInitializer.CALL_COUNT);
}

因为静态初始化块已经在类加载时执行,所以CALL_COUNT变量已经设置为1

void whenInitializingStaticInitializer_thenCallCountStaysOne() {
    StaticInitializer staticInitializer = new StaticInitializer();
    assertEquals(1, StaticInitializer.CALL_COUNT);
}

创建StaticInitializer的新实例时,CALL_COUNT仍然是1。我们无法再次调用静态初始化块。

5. 使用单元测试框架

JUnit和TestNG提供了注解来只运行一次设置方法。在JUnit中,使用@BeforeAll注解,而在TestNG或较旧的JUnit版本中,可以使用@BeforeClass注解来执行一次方法。

这是JUnit中此类设置方法的一个例子:

@BeforeAll
static void setup() {
    log.info("@BeforeAll - executes once before all test methods in this class");
}

6. 总结

在这篇文章中,我们学习了如何确保只执行一次方法的各种方法。在不同的场景中,例如初始化数据库连接,我们需要这种方式。

我们所看到的方法利用了锁定、AtomicBoolean和静态初始化。还可以使用单元测试框架来只执行一次方法。

如往常一样,所有这些示例的实现代码可以在GitHub上找到:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-methods