25

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

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

  1. 如果不保持同步会发生什么?
  2. synchronized getInstance()使用method 还是使用synchronizedblock inside是一种好习惯getInstance()

请告知是否有其他出路。

4

9 回答 9

40

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

public class MySingleton {

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

     private MySingleton () {}

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

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

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

顺便说一句,我期待着有一个@Singleton注释可以处理这些问题的那一天。

编辑:

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

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

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

但是,如果您按name加载类,情况就完全不同了。调用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()调用时创建的,这可能是静态初始化准备好执行之前。

于 2012-06-17T15:04:49.020 回答
18

鉴于您想让它真正线程安全,这个任务在理论上并不简单。

在@IBM找到了一篇关于这个问题的非常好的论文

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

然后您可能需要考虑 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 之类的注入框架来管理“单例”对象的实例化(并且您可以配置延迟初始化)。

于 2012-06-17T15:17:44.203 回答
5

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

于 2012-06-17T15:04:37.040 回答
3

这个问题实际上取决于创建实例的方式和时间。如果您的getInstance方法延迟初始化:

if(instance == null){
  instance = new Instance();
}

return instance

然后你必须同步,否则你可能会得到多个实例。这个问题通常在关于Double Checked Locking的讨论中得到处理。

否则,如果您预先创建一个静态实例

private static Instance INSTANCE = new Instance();

那么不需要同步该getInstance()方法。

于 2012-06-17T15:17:45.220 回答
3

有效java中描述的最佳方法是:

public class Singelton {

    private static final Singelton singleObject = new Singelton();

    public Singelton getInstance(){
        return singleObject;
    }
}

不需要同步。

于 2012-11-06T07:41:59.780 回答
1

没有人按照Effective Java中的建议使用 Enums吗?

于 2012-06-17T15:42:25.393 回答
0

如果您确定您的 java 运行时使用的是新的 JMM(Java 内存模型,可能比 5.0 更新),那么仔细检查锁就可以了,但是在实例前面添加一个 volatile。否则,您最好使用 Bohemian 所说的静态内部类,或 Florian Salihovic 所说的“Effective Java”中的 Enum。

于 2012-06-17T16:29:06.607 回答
0

答案已经在这里被接受,但我想分享测试来回答你的第一个问题。

如果不保持同步会发生什么?

SingletonTest当您在多线程环境中运行时,这将是完全灾难的类。

/**
 * @author MILAN
 */
public class SingletonTest
{
    private static final int        PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors();
    private static final Thread[]   THREADS         = new Thread[PROCESSOR_COUNT];
    private static int              instancesCount  = 0;
    private static SingletonTest    instance        = null;

    /**
     * private constructor to prevent Creation of Object from Outside of the
     * This class.
     */
    private SingletonTest()
    {
    }

    /**
     * return the instance only if it does not exist
     */
    public static SingletonTest getInstance()
    {
        if (instance == null)
        {
            instancesCount++;
            instance = new SingletonTest();
        }
        return instance;
    }

    /**
     * reset instancesCount and instance.
     */
    private static void reset()
    {
        instancesCount = 0;
        instance = null;
    }

    /**
     * validate system to run the test
     */
    private static void validate()
    {
        if (SingletonTest.PROCESSOR_COUNT < 2)
        {
            System.out.print("PROCESSOR_COUNT Must be >= 2 to Run the test.");
            System.exit(0);
        }
    }

    public static void main(String... args)
    {
        validate();
        System.out.printf("Summary :: PROCESSOR_COUNT %s, Running Test with %s of Threads. %n", PROCESSOR_COUNT, PROCESSOR_COUNT);
        
        long currentMili = System.currentTimeMillis();
        int testCount = 0;
        do
        {
            reset();

            for (int i = 0; i < PROCESSOR_COUNT; i++)
                THREADS[i] = new Thread(SingletonTest::getInstance);

            for (int i = 0; i < PROCESSOR_COUNT; i++)
                THREADS[i].start();

            for (int i = 0; i < PROCESSOR_COUNT; i++)
                try
                {
                    THREADS[i].join();
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
            testCount++;
        }
        while (instancesCount <= 1 && testCount < Integer.MAX_VALUE);
        
        System.out.printf("Singleton Pattern is broken after %d try. %nNumber of instances count is %d. %nTest duration %dms", testCount, instancesCount, System.currentTimeMillis() - currentMili);
    }
}

程序的输出清楚地表明您需要使用 getInstance 作为synchronized或添加synchronized锁来封闭新的 SingletonTest。

摘要 :: PROCESSOR_COUNT 32,使用 32 个线程运行测试。
单例模式在 133 次尝试后被破坏。
实例数为 30。
测试持续时间 500ms
于 2020-08-30T07:51:15.033 回答
0

为简单起见,我认为使用枚举类是一种更好的方法。我们不需要做任何同步。Java 通过构造,始终确保只创建一个常量,无论有多少线程试图访问它。

仅供参考,在某些情况下,您需要将单例替换为其他实现。然后我们需要修改类,这违反了 Open Close 原则。单例的问题是,由于具有私有构造函数,您无法扩展该类。因此,客户端通过接口进行交谈是一种更好的做法。

使用枚举类和接口实现单例:

客户端.java

public class Client{
    public static void main(String args[]){
        SingletonIface instance = EnumSingleton.INSTANCE;
        instance.operationOnInstance("1");
    }
}

SingletonIface.java

public interface SingletonIface {
    public void operationOnInstance(String newState);
}

EnumSingleton.java

public enum EnumSingleton implements SingletonIface{
INSTANCE;

@Override
public void operationOnInstance(String newState) {
    System.out.println("I am Enum based Singleton");
    }
}
于 2017-11-02T21:11:46.507 回答