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()
方法是线程安全的。
我们可以用以下测试验证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
和静态初始化。还可以使用单元测试框架来只执行一次方法。
如往常一样,所有这些示例的实现代码可以在GitHub上找到:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-methods。