在Java中仅执行一次方法

2025/04/20

1. 概述

在本教程中,我们将探索仅执行一次方法的方法,这在多种情况下非常有用。例如,初始化单例实例的方法,或执行一次性设置操作的方法。

我们将探索各种技术来确保某个方法仅被调用一次,这些技术包括使用布尔变量和synchronized关键字、AtomicBoolean以及静态初始化块。此外,某些单元测试框架(例如JUnitTestNG)提供了注解,可以帮助确保某个方法仅被执行一次。

2. 使用Boolean和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,因此不会执行初始化逻辑。

同步确保每次只有一个线程可以执行初始化方法,这可以防止多个线程同时执行初始化逻辑,从而避免潜在的竞争条件,我们需要使用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++;
    }
}

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

使用AtomicBoolean可以确保compareAndSet()方法是原子操作,这意味着同一时刻只有一个线程可以修改isInitialized的值。这样可以避免出现竞争条件,并确保initialize()方法是线程安全的。

我们可以通过以下测试验证我们只执行了一次AtomicBooleanInitializer的initializationLogic()方法:

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和静态初始化器,也可以使用单元测试框架来仅运行一次方法。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章