写时读锁

Read lock while Writing

我需要帮助来理解下面的代码:

private Predicate composedPredicate = null;

public boolean evaluate(Task taskData) {
        boolean isReadLock = false;
        try{
            rwl.readLock().lock();
            isReadLock = true;
            if (composedPredicate == null) {
                rwl.readLock().unlock();
                isReadLock = false;
                rwl.writeLock().lock();
                if (composedPredicate == null) {
                    //write to the "composedPredicate" object
                }
            }
        }finally {
            if (isReadLock) {
                rwl.readLock().unlock();
            }else{
                rwl.writeLock().unlock();
            }
        }
        return composedPredicate.test(taskData);
    }

如果我们在上面的代码中不使用Read Locks会怎么样呢? 喜欢:

public boolean evaluate(Task taskData) {
        //boolean isReadLock = false;
        try{
            //rwl.readLock().lock();
            //isReadLock = true;
            if (composedPredicate == null) {
                //rwl.readLock().unlock();
                //isReadLock = false;
                rwl.writeLock().lock();
                if (composedPredicate == null) {
                    //write to the "composedPredicate" object
                }
            }
        }finally {
            rwl.writeLock().unlock();
        }
        return composedPredicate.test(taskData);
    }
  1. 我们只写数据时真的需要读锁吗?
  2. 以上两个代码有什么区别?
  3. 即使是访问对象(composedPredicate) 是否也应该使用读取锁来进行空检查?

您发布的第一个代码是 Java 中使用 read/write 锁的双重检查锁定方法的正确实现。

你的第二个没有读锁的实现被破坏了。内存模型允许从另一个线程的角度对写入进行重新排序,以查看写入内存的结果。

可能发生的情况是,您可能在读取它的线程中使用了一个未完全初始化的 Predicate 实例。

您的代码示例:

我们有线程 A 和 B 运行ning evaluate 并且 composedPredicate 最初是 null

  1. A: 看到 composedPredicatenull
  2. A:写锁
  3. A:创建 Predicate
  4. 实现的实例
  5. A: 在构造函数中初始化这个实例
  6. A:将实例分配给共享变量composedPredicate
  7. A:解锁写锁
  1. B: 看到 composedPredicate 不是 null
  2. B: 运行s composedPredicate.test(taskData);
  3. 但是,编译器、JVM 或您系统的硬件架构重新排序了线程 A 的步骤 4 和 5,并在初始化之前分配了共享字段的 Predicate 实例的地址(这是允许的Java 内存模型)
  4. composedPredicate.test(taskData); 运行 使用未完全初始化的实例,并且您的代码在生产中出现随机意外错误,导致您的公司遭受巨大损失(可能发生这种情况..取决于系统你正在建造)

第 4 步和第 5 步的重新排序是否发生取决于许多因素。也许它只在系统负载很重的情况下才起作用。它可能根本不会发生在你的 OS、硬件、JVM 版本等上(但在下一个版本的 JVM、你的 OS 上,或者当你将应用程序移动到不同的物理机器时,它可能会突然开始发生)

坏主意。

此代码类似于使用同步块的旧 'Singleton-pattern'。例如

class Singleton
{
    volatile Singleton s;

    public static Singleton instance()
    {
        if(s == null)
        {
             synchronized(Singleton.class)
             {
                  if(s == null)
                      s = new Singleton();
             }
         }
         return s;
    }
}

注意双 'null-check',其中只有第二个同步。之所以做第一个 'null-check' 是为了防止在调用 instance() 方法时阻塞线程(因为当不为 null 时,它可以在没有同步的情况下继续进行)。

您的第一个代码也是如此。首先它检查是否有东西分配给 composedPredicate。如果不是这种情况,它只会获取一个 writingLock(阻止所有其他线程反对 readLocks,这只会阻止 writeLocks)。

'singleton-pattern' 和您的代码的主要区别在于您的值可以重新分配。这只有在确保没有人在修改期间读取该值的情况下才能安全地发生。通过删除 readLock,您基本上创建了一个线程在访问 composedPredicate 时可能会得到未定义结果(如果不是崩溃)的可能性,而另一个线程正在修改同一字段。

所以回答你的问题: 1.你不需要一个写锁,只需要一个写锁(这将阻止所有其他试图锁定的线程)。但在这种设计模式中,你不能将其遗漏。 2. & 3. 见上面的解释。

希望这足以让您掌握这种模式。

编辑 正如 Erwin Bolwidt 评论的那样,由于 compiler/CPU 代码优化(其中 read/write 操作可能会乱序发生),上述模式被认为是错误的(没有 'volatile' 关键字)。在链接的博客中,有针对此问题的 alternatives/fixes 示例。事实证明,'volatile' 关键字创建了一个障碍,它不允许编译器或 CPU 优化对读写操作进行重新排序,因此 'fixes' 上述 'double-checked-locking' 模式。