写时读锁
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);
}
- 我们只写数据时真的需要读锁吗?
- 以上两个代码有什么区别?
- 即使是访问对象(composedPredicate) 是否也应该使用读取锁来进行空检查?
您发布的第一个代码是 Java 中使用 read/write 锁的双重检查锁定方法的正确实现。
你的第二个没有读锁的实现被破坏了。内存模型允许从另一个线程的角度对写入进行重新排序,以查看写入内存的结果。
可能发生的情况是,您可能在读取它的线程中使用了一个未完全初始化的 Predicate 实例。
您的代码示例:
我们有线程 A 和 B 运行ning evaluate
并且 composedPredicate 最初是 null
。
- A: 看到
composedPredicate
是 null
- A:写锁
- A:创建
Predicate
实现的实例
- A: 在构造函数中初始化这个实例
- A:将实例分配给共享变量
composedPredicate
- A:解锁写锁
- B: 看到
composedPredicate
是 不是 null
- B: 运行s
composedPredicate.test(taskData);
- 但是,编译器、JVM 或您系统的硬件架构重新排序了线程 A 的步骤 4 和 5,并在初始化之前分配了共享字段的 Predicate 实例的地址(这是允许的Java 内存模型)
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' 模式。
我需要帮助来理解下面的代码:
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);
}
- 我们只写数据时真的需要读锁吗?
- 以上两个代码有什么区别?
- 即使是访问对象(composedPredicate) 是否也应该使用读取锁来进行空检查?
您发布的第一个代码是 Java 中使用 read/write 锁的双重检查锁定方法的正确实现。
你的第二个没有读锁的实现被破坏了。内存模型允许从另一个线程的角度对写入进行重新排序,以查看写入内存的结果。
可能发生的情况是,您可能在读取它的线程中使用了一个未完全初始化的 Predicate 实例。
您的代码示例:
我们有线程 A 和 B 运行ning evaluate
并且 composedPredicate 最初是 null
。
- A: 看到
composedPredicate
是null
- A:写锁
- A:创建
Predicate
实现的实例
- A: 在构造函数中初始化这个实例
- A:将实例分配给共享变量
composedPredicate
- A:解锁写锁
- B: 看到
composedPredicate
是 不是null
- B: 运行s
composedPredicate.test(taskData);
- 但是,编译器、JVM 或您系统的硬件架构重新排序了线程 A 的步骤 4 和 5,并在初始化之前分配了共享字段的 Predicate 实例的地址(这是允许的Java 内存模型)
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' 模式。