Java中的单例和多线程

viv*_*011 22 java singleton multithreading design-patterns

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

假设我有3个线程,并且所有线程都试图同时访问getInstance()singleton类的方法 -

  1. 如果不保持同步会发生什么?
  2. 在内部使用synchronized getInstance()方法或使用synchronized块是好的做法getInstance().

请告知是否还有其他方法.

Boh*_*ian 35

如果你谈论的是线程安全的,延迟初始化的的,这里是使用完成一个很酷的代码模式100%线程延迟初始化没有任何同步代码:

public class MySingleton {

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

     private MySingleton () {}

     public static MySingleton getInstance() {
         return MyWrapper.INSTANCE;
     }
}
Run Code Online (Sandbox Code Playgroud)

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

它的工作原理,因为类加载器都有自己的处理类的静态初始化同步:您保证所有静态初始化完成使用前级,并在此代码的类是内只使用getInstance()方法,所以这是上课的时候loading加载内部类.

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

编辑:

一个特别不相信的人声称包装类"什么都不做".这是证明它确实很重要,尽管在特殊情况下.

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

如果你只是简单地调用该getInstance()方法,那么几乎没有区别 - 不同之处在于,在使用包装版本创建实例之前,所有其他 sttic初始化都已完成,但这很容易通过简单地处理源中最后列出的静态实例变量.

但是,如果您按名称加载类,则故事情况则完全不同.调用要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;
    }
}
Run Code Online (Sandbox Code Playgroud)

主要:

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);
}
Run Code Online (Sandbox Code Playgroud)

输出:

LazySingleton was created 0 ms ago
EagerSingleton was created 1001 ms ago
Run Code Online (Sandbox Code Playgroud)

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


Pet*_*der 16

理论上,这项任务非常重要,因为您希望使其真正具有线程安全性.

@ 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;
}
Run Code Online (Sandbox Code Playgroud)

所以,你应该采取一些不那么懒惰的东西:

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

    private Singleton() { }

    public static Singleton getInstance() {
        return instance;
    }
}
Run Code Online (Sandbox Code Playgroud)

或者,更加臃肿,但更灵活的方法是避免使用静态单例并使用Spring等注入框架来管理"singleton-ish"对象的实例化(并且可以配置延迟初始化).

  • 你知道,这是一个破碎的模式.见[本文](http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html). (3认同)
  • “实例”字段需要 volatile 并且内部的双重锁定是无用的,因为 JVM 可以自由地粗化锁。在大多数硬件上,易失性读取实际上是免费的,所以不要害怕易失性*读取*。写入的成本要高得多,因为它们需要清空 CPU 缓冲区。这种方式提出的答案是完全错误的。 (2认同)

das*_*ght 5

getInstance只有在懒惰地初始化单例时才需要内部同步.如果您可以在线程启动之前创建实例,则可以在getter中删除同步,因为引用变为不可变.当然,如果单例对象本身是可变的,则需要同步其访问可以同时更改的信息的方法.