Java 中的 Singleton & Multithreading

在多线程环境中使用 Singleton 类的首选方式是什么?

假设我有 3 个线程,并且它们都尝试同时访问单例类的方法 -getInstance()

  1. 如果不保持同步,会发生什么情况?
  2. 在 里面使用方法或使用块是很好的做法吗?synchronizedgetInstance()synchronizedgetInstance()

如果有其他出路,请告知。


答案 1

如果您正在谈论线程安全单例延迟初始化,这里有一个很酷的代码模式,可以在没有任何同步代码的情况下完成100%线程安全的延迟初始化

public class MySingleton {

     private static class MyWrapper {
         static MySingleton INSTANCE = new MySingleton();
     }

     private MySingleton () {}

     public static MySingleton getInstance() {
         return MyWrapper.INSTANCE;
     }
}

这将仅在调用时实例化单例,并且它是100%线程安全的!这是一部经典之作。getInstance()

它之所以有效,是因为类装入器有自己的同步来处理类的静态初始化:可以保证在使用类之前所有静态初始化都已完成,并且在此代码中,类仅在方法中使用,因此这就是装入类加载内部类的时候。getInstance()

顺便说一句,我期待着有一天存在处理此类问题的注释。@Singleton

编辑:

一个特别的不信者声称包装类“什么都不做”。这里有证据证明它确实很重要,尽管是在特殊情况下。

基本区别在于,对于包装类版本,单例实例是在加载包装类时创建的,当第一次调用时,会进行单例实例,但对于非包装版本(即简单的静态初始化),实例是在加载主类时创建的。getInstance()

如果您只有对方法的简单调用,那么几乎没有区别 - 区别在于,在使用包装版本时,所有其他静态初始化都将在创建实例之前完成,但这可以通过简单地将静态实例变量列在源中的最后一个来轻松处理。getInstance()

但是,如果按名称加载类,则故事会大不相同。调用一个类,提示静态初始化发生,因此,如果要使用的单例类是服务器的属性,则使用简单版本时将创建静态实例,而不是调用时。我承认这有点人为的,因为你需要使用反射来获取实例,但尽管如此,这里有一些完整的工作代码可以演示我的论点(以下每个类都是一个顶级类):Class.forName(className)Class.forName()getInstance()

public abstract class BaseSingleton {
    private long createdAt = System.currentTimeMillis();

    public String toString() {
        return getClass().getSimpleName() + " was created " + (System.currentTimeMillis() - createdAt) + " ms ago";
    }
}

public class EagerSingleton extends BaseSingleton {

    private static final EagerSingleton INSTANCE = new EagerSingleton();

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

public class LazySingleton extends BaseSingleton {
    private static class Loader {
        static final LazySingleton INSTANCE = new LazySingleton();
    }

    public static LazySingleton getInstance() {
        return Loader.INSTANCE;
    }
}

主要有:

public static void main(String[] args) throws Exception {
    // Load the class - assume the name comes from a system property etc
    Class<? extends BaseSingleton> lazyClazz = (Class<? extends BaseSingleton>) Class.forName("com.mypackage.LazySingleton");
    Class<? extends BaseSingleton> eagerClazz = (Class<? extends BaseSingleton>) Class.forName("com.mypackage.EagerSingleton");

    Thread.sleep(1000); // Introduce some delay between loading class and calling getInstance()

    // Invoke the getInstace method on the class
    BaseSingleton lazySingleton = (BaseSingleton) lazyClazz.getMethod("getInstance").invoke(lazyClazz);
    BaseSingleton eagerSingleton = (BaseSingleton) eagerClazz.getMethod("getInstance").invoke(eagerClazz);

    System.out.println(lazySingleton);
    System.out.println(eagerSingleton);
}

输出:

LazySingleton was created 0 ms ago
EagerSingleton was created 1001 ms ago

如您所见,未包装的简单实现是在调用时创建的,这可能是在静态初始化准备好执行之前Class.forName()


答案 2

从理论上讲,该任务并非易事,因为您希望使其真正实现线程安全。

关于这个问题的一篇非常好的论文被发现@IBM

只是获取单例不需要任何同步,因为它只是一个读取。因此,只需同步同步的设置即可。除非两个胎面尝试在启动时同时创建单例,否则您需要确保检查实例是否设置了两次(一个在同步外部,一个在同步内部),以避免在最坏的情况下重置实例。

然后,您可能需要考虑 JIT(实时)编译器如何处理无序写入。此代码将有点接近解决方案,尽管无论如何都不会是100%线程安全的:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized(Singleton.class) {      
            Singleton inst = instance;         
            if (inst == null) {
                synchronized(Singleton.class) {  
                    instance = new Singleton();               
                }
            }
        }
    }
    return instance;
}

所以,你也许应该诉诸一些不那么懒惰的东西:

class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() { }

    public static Singleton getInstance() {
        return instance;
    }
}

或者,稍微臃肿一些,但更灵活的方法是避免使用静态单例,并使用注入框架(如Spring)来管理“单例”对象的实例化(您可以配置惰性初始化)。


推荐