如何在 Java 中正确创建线程安全的单例工厂? [复制]

Posted

技术标签:

【中文标题】如何在 Java 中正确创建线程安全的单例工厂? [复制]【英文标题】:How to correctly make a thread safe Singleton Factory in Java? [duplicate] 【发布时间】:2014-01-28 18:21:19 【问题描述】:

这是我第一次写Factory 类。下面是我的工厂类,我不确定这是否是使线程安全的单例工厂类的正确方法。我将使用这个工厂返回我的客户实例吗?

public class ClientFactory 

    private static ClientFactory instance = null;   

    private ClientFactory() 

    

    public static ClientFactory getInstance() 

        if (instance == null)
        
            instance =  new ClientFactory();
        

        return instance;
    

    public IClient getClient() 

        return new TestClient();
    

这是我的 TestClient 类 -

public class TestClient implements IClient 



这就是我将如何使用我的工厂 -

IClient client = ClientFactory.getInstance().getClient();

【问题讨论】:

您的工厂只有一个实例,每次调用 getClient 时都会返回一个新的 Client 实例。有什么问题? :) 这是制作只返回一个实例的工厂模式的正确方法吗?我正在阅读一些我也可以用于此目的的 IDIOM.. @SSH 忽略我之前的回答。你的工厂是单身人士。如果这就是你所追求的,那么你就在那里。而且你的使用没问题。 啊..这就是我要找的..我会相应地更新我的问题..如何制作线程安全的单例工厂?还有为什么它不是线程安全的,如果您也可以向我解释一下,那将有很大帮助.. 另请阅读:***.com/questions/70689/… 【参考方案1】:

事实上,您的工厂不是线程安全的,因为在竞争条件下,您可以在应用程序中拥有多个 ClientFactory。让我们假设两个线程:

    ThreadA 正在评估条件“if (instance == null)”且实例为 null,因此它进入语句 ThreadB 正在评估条件 'if (instance == null)' 并且 instance 为 null(因为 A 没有实例化它),所以它进入语句 ThreadA 创建新的 ClientFactory() 并返回它 ThreadB 创建新的 ClientFactory() 并返回它 现在我们在应用程序中有多个 ClientFactory。当然,稍后尝试检索实例的其他线程将始终返回单个实例。

在我看来,用 Java 编写单例最简单的方法是使用枚举。在你的情况下,它看起来:

public enum ClientFactory 
  INSTANCE;

  public Company getClient() 
    return new Company();
  

及用法:

ClientFactory.INSTANCE.getClient()

【讨论】:

@JakbuK:谢谢你的建议。你能详细解释一下为什么它不是线程安全的单例吗?我只是想了解.. @SSH 我已经扩展了我的答案 - 希望它会有所帮助【参考方案2】:

单件和工厂是不同的东西。要属性构造一个 Singleton,我想您可以将其 getInstance() 方法视为一个工厂。工厂制造“东西”。单例意味着在任何时候都只会存在 0 个或恰好 1 个这些“事物”。

如果你想创建一个合适的 Singleton,在 Java 中以线程安全的方式来做这件事是非常麻烦的。如果没有同步或其他线程安全对策,您上面列出的代码在 check-then-set 代码周围有一个微妙的竞争条件,以初始化 ClientFactory 实例变量。围绕这种竞争条件有两种方法。您选择哪种方式很大程度上取决于通过 ClientFactory 构造函数的成本。我的构造函数通常是轻量级的,所以我走的是避免同步需要的路径。

public class ClientFactory 
    private static final ClientFactory instance = new ClientFactory();

    private ClientFactory()  

    public static ClientFactory getInstance() 
        return instance;
    

如果你想在构造中“懒惰”,直到有人显式调用 getInstance() 才开始构建,现在需要同步以避免竞争条件。

public class ClientFactory 
    private static ClientFactory instance = null;

    private ClientFactory()  

    public static synchronized ClientFactory getInstance() 
        if ( instance == null ) 
            instance = new ClientFactory();
        
        return instance;
    

【讨论】:

谢谢.. 你能用一个例子详细解释一下为什么我的代码不是线程安全的单例吗?我只是想了解.. 考虑2个不同的线程几乎在同一时刻调用getInstance()的情况。他们都看到 instance == null 并且他们都 new ClientFactory() 将其分配给实例。他们都成功了,一切似乎都“奏效”了。但是,不仅创建了一个 ClientFactory(),还创建了两个;其中一个不再可达,因为没有人持有对它的引用。您的代码不是线程安全的,因为它缺乏围绕 check-then-set 的同步保护。 静态最终变体完全避免了同步问题并且通常工作得很好,但是静态构造函数中的失败是一个拖累追踪(没有方法名称的 StackTraces 真是太好了......不是)。 【参考方案3】:

Wiki 上的线程安全实现(示例) - Singleton Pattern on Wikipedia

与上面的链接一样,单元素 enum 类型是为任何支持枚举的 Java 实现 Singleton 的最佳方式。

最好但最简单的一个:

public class ClientFactory
    private ClientFactory() 

    private static ClientFactory INSTANCE=null;

    public static ClientFactory getInstance() 
        if(INSTANCE==null)
            synchronize(ClientFactory.class)
                if(INSTANCE==null) // check again within synchronized block to guard for race condition
                    INSTANCE=new ClientFactory();
            
        
        return INSTANCE;
    

Source:Wikipedia

【讨论】:

是的,请仔细检查。当我想传递一些参数来构造实例时,我怀疑枚举。【参考方案4】:

你的工厂是一个完美的单例(只是它不是线程安全的)。

【讨论】:

感谢您的建议。您能否详细解释一下为什么它不是线程安全的单例?我只是想了解.. @SSH 假设您的实例当前为空。假设两个线程几乎同时调用静态 getInstance() 方法。如果线程 2 在线程 1 调用同一行之后,但在线程 1 调用下一行之前,立即调用此行“if (instance == null)”,那么两个线程都会看到实例为空,并且都将继续并创建您的工厂实例(因此您现在将拥有 2 个实例),从而打破您的单例模式。 如果您将“单例”定义为“每个进程只有一个实例”,那么 OP 的代码不是完美的单例,因为它可能导致每个进程有多个实例.【参考方案5】:

ClientFactory 是一个工厂,但既不是单例工厂,也不是线程安全工厂。 在任何时候,当 ClientFactory.getInstance().getClinet() 被调用时,它都会返回一个新的 实例,所以它不是绝对的单例工厂。如果我们像这样修复方法

private IClient iclient;

public IClient getClient() 

    if ( iclient == null )
         iclient = new TestClient();
    

    return iclient ;

那么工厂是一个单例工厂,但它不是线程安全的。 假设如果有多个线程调用getInstance,所有线程都会找到 客户端工厂实例为空,所以他们将分别构造实例, 问题与getClient()方法相同。

很容易修复,你可以将这两个方法声明为同步。

首先,如果你真的想使用工厂模式,别忘了隐藏客户端的构造函数

private TestClient()

【讨论】:

以上是关于如何在 Java 中正确创建线程安全的单例工厂? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

C#中各版本的单例模式

线程安全的单例模式

C++ 线程安全的单例模式总结

java 实现线程安全的单例模式

JAVA如何正确地写出单例模式(转)

线程安全的单例模式是否真的安全