1. 概述

在这个教程中,我们将讨论Bill Pugh的单例实现。单例模式有多种实现方式,其中懒加载单例和预加载单例尤为突出。此外,它们还支持同步和非同步版本。

Bill Pugh的单例实现支持懒加载单例对象。接下来的章节将探讨其实现细节,并揭示它如何解决其他实现面临的挑战。

2. 单例模式的基本原则

单例 是一种构造设计模式。顾名思义,这个模式帮助我们在整个应用中创建一个类的唯一实例。常用于创建成本高、耗时长的对象,如连接工厂、REST适配器、DAO等类。

在继续之前,先了解一下Java类单例实现的基本原则:

  1. 私有构造函数以防止通过new关键字实例化。
  2. 公共静态方法,通常命名为getInstance(),返回该类的单个实例。
  3. 私有静态变量用于存储类的唯一实例。

在多线程环境中限制一个类的单一实例并延迟实例化直到被引用时,可能会遇到挑战。因此,某些实现优于其他。考虑到这些挑战,我们将看到Bill Pugh的单例实现为何脱颖而出。

3. Bill Pugh单例实现

大多数单例实现通常面临以下挑战之一或两者:

  1. 预加载(Eager Loading)
  2. 因同步带来的开销

借助私有静态内部类,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上找到。