【问题标题】:Thread-safe update of shared object without performance penalty?共享对象的线程安全更新没有性能损失?
【发布时间】:2014-08-29 06:29:51
【问题描述】:

我正在努力解决我的类的线程安全实现,以便在 Servlet 或代码中的多个“客户端”之间共享。

假设我有以下MySingleton 类,这是一个使用Configuration 对象初始化的单例。但是,Configuration 对象可以被观察到,因此如果发生更改,单例将订阅通知。重要的关键点:

  • 配置可以随时更改(无法预测)

  • 配置解析时,其值保存到MySingleton的成员字段中

  • 单例的公共方法使用这些字段来生成返回结果

参见下面的简化代码:

public class MySingleton 
    implements IConfigurationObserver {
    // Member(s)
    private static volatile MySingleton instance;
    private final Configuration configuration;
    private String firstParam;
    private String secondParam;

    // Constructor(s)
    private MySingleton(Configuration configuration) {
        this.configuration = configuration;
        parseConfiguration();
        configuration.addObserver(this);
    }

    public static MySingleton getInstance(Configuration configuration) {
        // Perform synchronized creation if applicable (double locking technique)
        MySingleton syncInstance = instance;
        if (syncInstance == null) {
            synchronized(MySingleton.class) {
                syncInstance = instance; // Verify once again after synchronizing
                if(syncInstance == null) {  
                    syncInstance = new MySingleton(configuration);
                    instance = syncInstance;
                }
            }
        }

        return syncInstance;
    }

    // Private Method(s)
    private void parseConfiguration() {
        // Import values from the configuration
        this.firstParam = configuration.get(0);
        this.secondParam = configuration.get(1);
    }

    // Public Method(s)
    public String buildSentence() {
        // Build a new string based on values pulled from the configuration
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append(firstParam);
        strBuilder.append(" - ");
        strBuilder.append(secondParam);

        return strBuilder.toString();
    }

    // Observer Method(s)
    @Override
    public void onConfigurationUpdated() {
        // The configuration has changed. Parse it again.
        parseConfiguration();
    }
}

该类本身可以正常工作(在单线程环境中),但这里有一些我想在多线程场景中消除的威胁:

  • 如果在很短的时间内对Configuration 进行了两次更新,则对parseConfiguration() 的第一次调用可能在第二次调用开始之前尚未完成。这个很容易解决,我可以让parseConfiguration()同步(对吗?)。但是……

  • 当配置通知我们的单例时,假设我们正在调用buildSentence()。我不希望 buildSentence() 混合使用 firstParam 和 secondParam 的旧值(即,如果 parseConfiguration() 完成一半)。因此,我可以为parseConfiguration() 和buildSentence() 在Configuration 对象上放置一个同步块,但是我的性能受到严重影响:我不能同时调用多个buildSentence()。事实上,对我来说理想的情况是:

    • 如果buildSentence() 正在运行并且发生Configuration 更新,则parseConfiguration() 必须等到buildSentence() 结束才能运行

    • 如果parseConfiguration() 正在运行,则对buildSentence() 的调用必须等到parseConfiguration() 结束后才能开始

    • 但是,一旦parseConfiguration() 完成,我想允许多个线程同时运行buildSentence()。仅当更新即将发生或正在发生时才应进行锁定。

如何重构 MySingleton 以支持我上面列出的理想“规则”?有可能吗?


我一直在考虑一个涉及信号量的解决方案。即:执行buildSentence()时,检查信号量是否可用。如果是,请继续(不阻塞)。如果没有,请等待。而parseConfiguration 将在信号量执行期间锁定信号量。但是,如果有人有直接的建议方法,我不想过度设计这个问题。告诉我!

【问题讨论】:

  • 一个很好的 resource 用于 java 中的单例。
  • 我认为好的策略可能是将 Configuration 拆分为两个类 - 一个是 Configuration 本身作为不可变值类,可以传递给观察者,观察者负责以原子方式交换它,第二个类是ConfigurationHolder,保存当前的Configuration值,负责通知观察者。通过这种设计,parseConfiguration 不会在过程中间更改配置的问题,并且 buildSentence 始终使用当前一致的配置值。你认为这可行吗?
  • 没有性能损失的线程之间共享对象的方法。处罚可以很小。
  • "因此,我可以在 parseConfiguration() 和 buildSentence() 的配置对象上放置一个同步块,但随后我的性能受到严重影响" =>你测量过吗?除非存在高争用并且您每秒调用 parseConfiguration 数百万次,否则性能损失可能比您想象的要少得多。
  • @assylias +1 并注明 OP 每秒会调用 buildSentence 数百万次。

标签: java multithreading semaphore synchronized


【解决方案1】:

我想我会选择这样的:

public class MySingleton 
    implements IConfigurationObserver {
    // Member(s)
    private static volatile MySingleton instance;
    private final Configuration configuration;
    private volatile ParsedConfiguration currentConfig;

    // Constructor(s)
    private MySingleton(Configuration configuration) {
        this.configuration = configuration;
        parseConfiguration();
        configuration.addObserver(this);
    }

    public static MySingleton getInstance(Configuration configuration) {
        // Perform synchronized creation if applicable (double locking technique)
        MySingleton syncInstance = instance;
        if (syncInstance == null) {
            synchronized(MySingleton.class) {
                syncInstance = instance; // Verify once again after synchronizing
                if(syncInstance == null) {  
                    syncInstance = new MySingleton(configuration);
                    instance = syncInstance;
                }
            }
        }

        return syncInstance;
    }

    // Private Method(s)
    private void parseConfiguration() {
        // Import values from the configuration
        currentConfig = configuration.getNewParsedConfiguration();
    }

    // Public Method(s)
    public String buildSentence() {
        // Build a new string based on values pulled from the current configuration
        ParsedConfiguration configInUse = currentConfig;
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append(configInUse.getFirstParam());
        strBuilder.append(" - ");
        strBuilder.append(configInUse.getSecondParam());

        return strBuilder.toString();
    }

    // Observer Method(s)
    @Override
    public void onConfigurationUpdated() {
        // The configuration has changed. Parse it again.
        parseConfiguration();
    }
}

注意将currentConfig 移动到buildSentence 开头的局部变量的安全措施,因此对使用的ParsedConfig 的引用不能更改中间方法。

【讨论】:

  • 我不确定getNewParsedConfiguration() 是否必须是synchronized - 你能解释一下原因吗? OP 注意:ParsedConfiguration 确实需要是不可变的(不仅仅是有效的不可变)才能使其工作。
  • @Duncan 不仅必须被同步,而且如果是,它也无助于线程安全。相反,currentConfig 必须是 volatile。
  • @MarkoTopolnik 同意,如果没有volatile,某些线程可能会看到currentConfig 的旧值。
  • @Duncan Plus,使用 volatile 你只需要一个有效不可变对象。
  • @Duncan:再想一想你应该是对的——getNewParsedConfiguration 不一定是synchronized,因为两次调用只返回两个新对象。可能存在可能需要这样做的实施问题,但又可能没有。我会删除那部分。
猜你喜欢
  • 1970-01-01
  • 2011-08-02
  • 2011-09-26
  • 1970-01-01
  • 2012-08-19
  • 2021-05-27
  • 1970-01-01
  • 1970-01-01
  • 2020-11-30
相关资源
最近更新 更多