0

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

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)进行空检查?
4

2 回答 2

2

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

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

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

您的代码示例:

我们有线程 A 和 B 都在运行evaluate,并且null最初是组合Predicate。

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

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

馊主意。

于 2018-02-08T06:11:58.403 回答
-1

此代码类似于使用同步块的旧“单例模式”。例如

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'的原因是为了防止在instance()调用方法时阻塞线程(因为当不为null时,它可以在不同步的情况下继续进行)。

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

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

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

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

编辑 正如 Erwin Bolwidt 所评论的,由于编译器/CPU 代码优化(读/写操作可能无序发生),上述模式被认为是损坏的(没有“volatile”关键字)。在链接的博客中,有针对此问题的替代/修复示例。事实证明,“volatile”关键字创建了一个屏障,它不允许编译器或 CPU 优化对读写操作进行重新排序,从而“修复”了上述“双重检查锁定”模式。

于 2018-02-08T06:15:06.530 回答