双重检查锁定 - 陷阱?

时间:2012-05-04 18:14:01

标签: java synchronization double-checked-locking

我正在使用java大约一个月,而且我一般都是编程的业余爱好者,所以如果我出错了,请随时纠正我。也许我会提供一些过多的细节,但我现在很困惑,我不能再决定什么了。

所以,我一直在开发多线程客户端 - 服务器应用程序。所有线程都使用相同的对象,其中存储了某些配置值和共享记录器;此对象在服务器线程中初始化,然后作为参数传递给客户端线程类构造函数。首先,假设当服务器启动时,该对象的字段只更改一次,因此并发访问无需担心,但现在需要在修改时从配置文件中重新读取某些配置值,而不必重启服务器。

经过一些研究后想到的第一个想法是创建一个同步方法,当请求类中的某些值时将调用该方法,如果我们的配置文件自上次访问后发生更改并立即返回,则会重新读取这些值否则,像这样:

<This code is inside "config" class, instance of which is shared between threads>
private static long lastModified;
private static File configFile;

public class ChangingVariableSet
    {
    <changing variables go here>
    }

private synchronized void ReReadConfig
    {
    long tempLastMod = configFile.lastModified();
    if(lastModified == tempLastMod)
        return;
    <reread values here>
    lastModified = tempLastMod;
    }

public ChangingVariableSet GetValues()
    {
    ReReadConfig();
    <return necessary values>
    }

(上面的代码没有经过测试,我只是想了解一般的想法。)

但我只是不喜欢阻止每次请求值的想法,因为这看起来很昂贵,而且我的应用程序有可能变得很高,有很多线程在将来。所以我有一个“好”的想法 - 在锁定之前检查文件是否被修改,然后再次在锁定方法内部,以尽可能避免锁定:

 public ChangingVariableSet GetValues()
    {
    if(lastModified == configFile.lastModified())
        ReReadConfig();
    <return necessary values>
    }

十分钟后,我学会了它被称为双重检查锁定,另一个十分钟后我的世界在阅读this article后崩溃了两次:第一次当我学会它时,据说由于内部CPU缓存而无法工作,第二次是我读到有关long / float类型不是原子的操作。或者它是否会起作用,因为不涉及对象创建?并且,由于long上的操作是非原子的,将“lastModified”声明为volatile是否足够?如果可能的话,我更愿意正确解释它为什么会/不会起作用。提前谢谢。

PS:我知道类似的问题已经回答了几次,也许最好停止挑剔并同步整个“getValue”方法而不是“ReReadConfig”,但我正在努力学习更多关于线程的知识 - 安全编程并查看我自己的代码中的缺陷,以避免将来出现类似情况。我也为任何可能的语法和拼写错误道歉,我不太懂英语。


编辑:首先,我修改了最后一个“if”子句的拼写错误。第二 - 警告,上面的代码不是线程安全的,不要使用它!方法

 public ChangingVariableSet GetValues()
    {
    if(lastModified == configFile.lastModified())
        ReReadConfig();
    <return necessary values>
    }

如果文件在if-check和值返回之间的时间跨度内更新,则线程B可以在线程A开始返回值之前启动ReReadConfig,从而导致对必要数据的危险部分更改。在没有过多阻塞的情况下执行我需要的正确方法似乎是使用ReentrantReadWriteLock,但是,我仍然希望使用双重检查以避免过多(并且昂贵的,文件被假定为大型XML)配置重新读取:

<...>
private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static final Lock read  = readWriteLock.readLock();
private static final Lock write = readWriteLock.writeLock();

private void ReReadConfig
    {
    write.lock();
    long tempLastMod = configFile.lastModified();
    if(lastModified == tempLastMod)
        return;
    <reread values here>
    lastModified = tempLastMod;
    write.release();
    }

 public ChangingVariableSet GetValues()
    {
    if(lastModified == configFile.lastModified())
        ReReadConfig();
    read.lock();
    <get necessary values>
    read.release();
    <return necessary values>
    }

现在它看起来至少看起来是线程安全的,但是在检查时依赖于volatile“lastModified”变量仍然存在问题:我已经读过某些地方,即volatile变量无法保证非原子操作的任何内容,并且“long”类型读/写是非原子的。

2 个答案:

答案 0 :(得分:5)

您想使用ReadWriteLock。只要没有作家,这就不会阻止读者。

答案 1 :(得分:1)

如果您可以组织代码,使得所有配置数据都在一个不可变对象中,并且存在对该对象的共享volatile引用,那么这将是线程安全的。这种用例实际上是volatile语义的修订目标。

public static volatile Config config;

void rereadConfig() {
  if (modified)
    config = new Config(...);
}