1. 概述
在这个教程中,我们将讨论Bill Pugh的单例实现。单例模式有多种实现方式,其中懒加载单例和预加载单例尤为突出。此外,它们还支持同步和非同步版本。
Bill Pugh的单例实现支持懒加载单例对象。接下来的章节将探讨其实现细节,并揭示它如何解决其他实现面临的挑战。
2. 单例模式的基本原则
单例 是一种构造设计模式。顾名思义,这个模式帮助我们在整个应用中创建一个类的唯一实例。常用于创建成本高、耗时长的对象,如连接工厂、REST适配器、DAO等类。
在继续之前,先了解一下Java类单例实现的基本原则:
- 私有构造函数以防止通过
new
关键字实例化。 - 公共静态方法,通常命名为
getInstance()
,返回该类的单个实例。 - 私有静态变量用于存储类的唯一实例。
在多线程环境中限制一个类的单一实例并延迟实例化直到被引用时,可能会遇到挑战。因此,某些实现优于其他。考虑到这些挑战,我们将看到Bill Pugh的单例实现为何脱颖而出。
3. Bill Pugh单例实现
大多数单例实现通常面临以下挑战之一或两者:
- 预加载(Eager Loading)
- 因同步带来的开销
借助私有静态内部类,Bill Pugh或持有者单例模式解决了这两个问题:
public class BillPughSingleton {
private BillPughSingleton() {
}
private static class SingletonHelper {
private static final BillPughSingleton BILL_PUGH_SINGLETON_INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.BILL_PUGH_SINGLETON_INSTANCE;
}
}
Java应用中的类加载器仅在多个线程调用getInstance()
时,一次加载静态内部类SingletonHelper
到内存中。值得注意的是,我们并未使用同步,这消除了在访问同步方法时锁定和解锁对象的开销。所以这种方法弥补了其他实现的不足。
现在,让我们看看它是如何工作的:
@Test
void givenSynchronizedLazyLoadedImpl_whenCallgetInstance_thenReturnSingleton() {
Set<BillPughSingleton> setHoldingSingletonObj = new HashSet<>();
List<Future<BillPughSingleton>> futures = new ArrayList<>();
ExecutorService executorService = Executors.newFixedThreadPool(10);
Callable<BillPughSingleton> runnableTask = () -> {
logger.info("run called for:" + Thread.currentThread().getName());
return BillPughSingleton.getInstance();
};
int count = 0;
while(count < 10) {
futures.add(executorService.submit(runnableTask));
count++;
}
futures.forEach(e -> {
try {
setHoldingSingletonObj.add(e.get());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
});
executorService.shutdown();
assertEquals(1, setHoldingSingletonObj.size());
}
在上述方法中,多个线程并发地调用getInstance()
,但始终返回相同的对象引用。
4. Bill Pugh与非同步实现的对比
我们将在单线程和多线程环境中实现单例模式,多线程环境下的实现不使用synchronized
关键字。
4.1. 懒加载单例实现
基于前面描述的基本原则,让我们实现一个单例类:
public class LazyLoadedSingleton {
private static LazyLoadedSingleton lazyLoadedSingletonObj;
private LazyLoadedSingleton() {
}
public static LazyLoadedSingleton getInstance() {
if (null == lazyLoadedSingletonObj) {
lazyLoadedSingletonObj = new LazyLoadedSingleton();
}
return lazyLoadedSingletonObj;
}
}
LazyLoadedSingleton
对象只在getInstance()
方法被调用时创建,这就是懒加载。然而,在多个线程并发调用getInstance()
时,由于脏读,这会导致失败。即使没有使用同步,Bill Pugh的实现也不存在这个问题。
让我们看看LazyLoadedSingleton
类是否只创建一个对象:
@Test
void givenLazyLoadedImpl_whenCallGetInstance_thenReturnSingleInstance() throws ClassNotFoundException {
Class bs = Class.forName("com.baledung.billpugh.LazyLoadedSingleton");
assertThrows(IllegalAccessException.class, () -> bs.getDeclaredConstructor().newInstance());
LazyLoadedSingleton lazyLoadedSingletonObj1 = LazyLoadedSingleton.getInstance();
LazyLoadedSingleton lazyLoadedSingletonObj2 = LazyLoadedSingleton.getInstance();
assertEquals(lazyLoadedSingletonObj1.hashCode(), lazyLoadedSingletonObj2.hashCode());
}
这段代码试图利用反射API和getInstance()
方法通过反射创建LazyLoadedSingleton
实例。然而,使用反射创建失败,而getInstance()
始终返回该类的单个实例。
4.2. 预加载单例实现
前一节讨论的实现仅适用于单线程环境。但在多线程环境中,我们可以考虑使用类级别的静态变量来采用不同的方法:
public class EagerLoadedSingleton {
private static final EagerLoadedSingleton EAGER_LOADED_SINGLETON = new EagerLoadedSingleton();
private EagerLoadedSingleton() {
}
public static EagerLoadedSingleton getInstance() {
return EAGER_LOADED_SINGLETON;
}
}
类级别的变量EAGER_LOADED_SINGLETON
是静态的,因此当应用启动时,即使不需要,也会立即加载。然而,如前所述,Bill Pugh的实现支持单线程和多线程环境下的懒加载。
让我们看看EagerLoadedSingleton
类的运作:
@Test
void givenEagerLoadedImpl_whenCallgetInstance_thenReturnSingleton() {
Set<EagerLoadedSingleton> set = new HashSet<>();
List<Future<EagerLoadedSingleton>> futures = new ArrayList<>();
ExecutorService executorService = Executors.newFixedThreadPool(10);
Callable<EagerLoadedSingleton> runnableTask = () -> {
return EagerLoadedSingleton.getInstance();
};
int count = 0;
while(count < 10) {
futures.add(executorService.submit(runnableTask));
count++;
}
futures.forEach(e -> {
try {
set.add(e.get());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
});
executorService.shutdown();
assertEquals(1, set.size());
}
在上述方法中,多个线程调用runnableTask
,然后run()
方法调用getInstance()
获取EagerLoadedSingleton
的实例。然而,每次getInstance()
都返回该对象的一个实例。
这在多线程环境中工作,但表现出预加载,这是明显的缺点。
5. Bill Pugh与同步单例实现的对比
先前,我们在单线程环境中看到了LazyLoadedSingleton
。现在,我们修改它以支持多线程环境下的单例模式:
public class SynchronizedLazyLoadedSingleton {
private static SynchronizedLazyLoadedSingleton synchronizedLazyLoadedSingleton;
private SynchronizedLazyLoadedSingleton() {
}
public static synchronized SynchronizedLazyLoadedSingleton getInstance() {
if (null == synchronizedLazyLoadedSingleton) {
synchronizedLazyLoadedSingleton = new SynchronizedLazyLoadedSingleton();
}
return synchronizedLazyLoadedSingleton;
}
}
有趣的是,通过在getInstance()
方法上使用synchronized
关键字,我们限制了线程同时访问它。我们可以使用双重检查锁定方法来实现更高效的版本。
然而,Bill Pugh的实现明显胜出,因为它可以在没有同步开销的情况下在多线程环境中使用。
让我们确认这在多线程环境中是否有效:
@Test
void givenSynchronizedLazyLoadedImpl_whenCallgetInstance_thenReturnSingleton() {
Set<SynchronizedLazyLoadedSingleton> setHoldingSingletonObj = new HashSet<>();
List<Future<SynchronizedLazyLoadedSingleton>> futures = new ArrayList<>();
ExecutorService executorService = Executors.newFixedThreadPool(10);
Callable<SynchronizedLazyLoadedSingleton> runnableTask = () -> {
logger.info("run called for:" + Thread.currentThread().getName());
return SynchronizedLazyLoadedSingleton.getInstance();
};
int count = 0;
while(count < 10) {
futures.add(executorService.submit(runnableTask));
count++;
}
futures.forEach(e -> {
try {
setHoldingSingletonObj.add(e.get());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
});
executorService.shutdown();
assertEquals(1, setHoldingSingletonObj.size());
}
就像EagerLoadedSingleton
一样,SynchronizedLazyLoadedSingleton
类在多线程设置下也返回单个对象。但这次程序以懒加载的方式加载单例对象,不过由于同步带来了额外的开销。
6. 结论
在这篇文章中,我们比较了Bill Pugh的单例实现与其他常见的单例实现。Bill Pugh的单例实现性能更好,支持懒加载。因此,许多应用程序和库广泛使用它。
如往常一样,本文中的代码可以在GitHub上找到。