Java 写时读锁

Java 写时读锁,java,readwritelock,reentrantreadwritelock,Java,Readwritelock,Reentrantreadwritelock,我需要帮助理解以下代码: private Predicate composedPredicate = null; public boolean evaluate(Task taskData) { boolean isReadLock = false; try{ rwl.readLock().lock(); isReadLock = true; if (composedPredicate ==

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

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);
    }
如果在上述代码中不使用读锁,会发生什么? 比如:

  • 当我们只写数据时,我们真的需要读锁吗
  • 以上两种代码之间的区别是什么
  • 我们是否应该使用读锁来访问对象(composedPredicate)进行空检查
    您发布的第一个代码是Java中使用读/写锁的双重检查锁定方法的正确实现

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

    可能发生的情况是,您可能正在读取谓词的线程中使用未完全初始化的谓词实例

    代码示例:

    我们让线程A和B都运行
    evaluate
    ,并且composedPredicate最初为
    null

  • A:sees
    composedPredicate
    is
    null
  • 答:写锁
  • A:创建
    谓词
  • 答:在构造函数中初始化此实例
  • A:将实例分配给共享变量
    composedPredicate
  • A:解锁写锁
  • B:sees
    composedPredicate
    不是
    null
  • B:运行
    composedPredicate.test(taskData)
  • 但是,编译器、JVM或系统的硬件体系结构对线程A的步骤4和步骤5重新排序,并在初始化共享字段之前为其分配了谓词实例的地址(Java内存模型允许这样做)
  • composedPredicate.test(taskData)
    是使用未完全初始化的实例运行的,并且您的代码在生产过程中随机出现意外错误,从而给您的公司造成巨大损失(可能会发生这种情况..取决于您正在构建的系统)
  • 第4步和第5步的重新排序是否发生取决于许多因素。也许它只在系统负载很重的情况下才起作用。它可能根本不会发生在您的操作系统、硬件、JVM版本等上(但在JVM的下一个版本、您的操作系统上,或者当您将应用程序移动到另一台物理机器上时,它可能会突然发生)


    坏主意。

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

    class Singleton
    {
        volatile Singleton s;
    
        public static Singleton instance()
        {
            if(s == null)
            {
                 synchronized(Singleton.class)
                 {
                      if(s == null)
                          s = new Singleton();
                 }
             }
             return s;
        }
    }
    
    注意双“空检查”,其中只有第二个同步。执行第一个“null检查”的原因是为了防止在调用
    instance()
    方法时阻塞线程(因为当非null时,它可以在不同步的情况下继续)

    您的第一个代码也在这样做。首先,它检查是否有分配给
    composedPredicate
    的内容。如果不是这样的话,它只会获得一个写锁(它会阻止所有其他读锁线程,而读锁只会阻止写锁)

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

    因此,要回答您的问题: 1.写入时不需要读锁,只需要写锁(它将阻止所有其他试图锁定的线程)。但在这种设计模式中,您不能忽略它。 2. & 3.见上面的解释

    希望这足以掌握这种模式

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

    您在答案中发布的单例的双重检查锁定被破坏。它的问题与OP发布的第二个代码示例(没有读锁)的问题相同(最终可能会在线程中使用未初始化或部分初始化的单例,该单例只进行读取而不进行同步)。读这篇文章:啊,很有趣。我知道它被认为是“糟糕的设计”,但我不知道它坏了。这就是你每天学习的方式。谢谢你的提示!谢谢你的解释!还有一个问题是:假设在我给出的第二个代码中,如果我们在第二次空检查后使用函数获取初始化对象,我们需要读锁吗?因为,对象将在函数调用执行后返回。例如:第二次空检查:如果(composedPredicate==null){composedPredicate=getInitialisedObject();}编译器、JVM和硬件体系结构仍然可以对对象的初始化和对引用的写入重新排序,甚至可以跨越方法边界。唯一能阻止这种(潜在的)重新排序的是在中定义的“发生在之前”关系。建立这种关系的典型方法是
    同步
    易失性
    ,以及
    java.util.concurrent
    中许多类中的方法。
    class Singleton
    {
        volatile Singleton s;
    
        public static Singleton instance()
        {
            if(s == null)
            {
                 synchronized(Singleton.class)
                 {
                      if(s == null)
                          s = new Singleton();
                 }
             }
             return s;
        }
    }