Bill Pugh单例实现

2025/04/20

1. 概述

在本教程中,我们将讨论Bill Pugh的单例模式实现。单例模式有多种实现,其中,延迟加载单例模式和预先加载单例模式最为突出。此外,它们也支持同步和非同步版本。

Bill Pugh的单例模式实现支持延迟加载的单例对象,在接下来的章节中,我们将探索它的实现,并看看它如何解决其他实现所面临的挑战。

2. 单例实现的基本原理

单例模式是一种创建型设计模式,顾名思义,这种设计模式用于创建类的单个实例,该实例将在整个应用程序中使用。它通常用于创建成本高且耗时的类,例如连接工厂、REST适配器、DAO等。

在继续之前我们先来了解一下Java类单例实现的基本原理:

  • 私有构造函数以防止使用new运算符进行实例化
  • 最好使用名为getInstance()的公共静态方法,以返回类的单个实例
  • 私有静态变量,用于存储类的唯一实例

此外,在多线程环境中限制类的单个实例,并将实例的初始化推迟到被引用时,可能会面临挑战。因此,这就是一种实现比其他实现更胜一筹的地方。考虑到这些挑战,我们将看看Bill Pugh的单例实现如何脱颖而出。

3. Bill Pugh Singleton实现

大多数情况下,Singleton实现面临以下一个或两个挑战:

  • 预先加载
  • 同步造成的开销

Bill Pugh或Holder Singleton模式借助私有静态内部类解决了这两个问题

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应用程序中的类加载器只会在内存中加载一次静态内部类SingletonHelper,即使多个线程调用getInstance()也是如此。值得注意的是,我们也没有使用同步方法,这消除了访问同步方法时锁定和释放对象的开销。因此,这种方法解决了其他实现所面临的缺陷。

现在,让我们看看它是如何工作的:

@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的实现即使没有使用synchronized,也不会出现这种情况

让我们看看LazyLoadedSingleton类是否只创建一个对象:

@Test
void givenLazyLoadedImpl_whenCallGetInstance_thenReturnSingleInstance() throws ClassNotFoundException {
    Class bs = Class.forName("cn.tuyucheng.taketoday.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()始终返回LazyLoadedSingleton类的单个实例。

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单例实现性能更佳,并且支持延迟加载。因此,许多应用程序和库都广泛使用它。

Show Disqus Comments

Post Directory

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