Java 8 中的响应式异步搜索
Responsive asynchronous search-as-you-type in Java 8
我正在尝试在 Java 中实现“键入时搜索”模式。
设计的目标是不会丢失任何更改,但与此同时,(耗时的)搜索操作应该能够提前中止并尝试更新的模式。
这是我到目前为止的结果(Java 8 伪代码):
AtomicReference<String> patternRef
AtomicLong modificationCount
ReentrantLock busy;
Consumer<List<ResultType>> resultConsumer;
// This is called in a background thread every time the user presses a key
void search(String pattern) {
// Update the pattern
synchronized {
patternRef.set(pattern)
modificationCount.inc()
}
try {
if (!busy.tryLock()) {
// Another search is already running, let it handle the change
return;
}
// Get local copy of the pattern and modCount
synchronized {
String patternCopy = patternRef.get();
long modCount = modificationCount.get()
}
while (true) {
// Try the search. It will return false when modificationCount changes before the search is finished
boolean success = doSearch(patternCopy, modCount)
if (success) {
// Search completed before modCount was changed again
break
}
// Try again with new pattern+modCount
synchronized {
patternCopy = patternRef.get();
modCount = modificationCount.get()
}
}
} finally {
busy.unlock();
}
}
boolean doSearch(String pattern, long modCount)
... search database ...
if (modCount != modificationCount.get()) {
return false;
}
... prepare results ...
if (modCount != modificationCount.get()) {
return false;
}
resultConsumer.accept(result); // Consumer for the UI code to do something
return modCount == modificationCount.get();
}
我是不是漏掉了重要的一点?竞争条件或类似情况?
Java8 中有什么东西可以使上面的代码更简单吗?
这段代码的根本问题可以概括为“试图通过多个不同的原子构造来实现原子性”。多个原子构造的组合不是原子的,尝试重新建立原子性会导致代码非常复杂、通常会损坏且效率低下。
在你的例子中,doSearch
的最后一次检查 modCount == modificationCount.get()
在仍然持有锁的情况下发生。在那之后,另一个线程(或多个其他线程)可以更新搜索字符串和 mod 计数,然后找到被占用的锁,因此,得出另一个搜索是 运行 的结论并会处理。
但是在最后一次 modCount == modificationCount.get()
检查之后那个线程并不关心。调用者只执行 if (success) { break; }
,然后是 finally { busy.unlock(); }
和 return。
所以答案是,是的,您有潜在的竞争条件。
因此,与其选择两个原子变量 synchronized
块和一个 ReentrantLock
,不如使用一个原子构造,例如单个原子变量:
final AtomicReference<String> patternRef = new AtomicReference<>();
Consumer<List<ResultType>> resultConsumer;
// This is called in a background thread every time the user presses a key
void search(String pattern) {
if(patternRef.getAndSet(pattern) != null) return;
// Try the search. doSearch will return false when not completed
while(!doSearch(pattern) || !patternRef.compareAndSet(pattern, null))
pattern = patternRef.get();
}
boolean doSearch(String pattern) {
//... search database ...
if(pattern != (Object)patternRef.get()) {
return false;
}
//... prepare results ...
if(pattern != (Object)patternRef.get()) {
return false;
}
resultConsumer.accept(result); // Consumer for the UI code to do something
return true;
}
这里,null
的值表示没有搜索是运行,所以如果后台线程将它设置为非null
值并找到旧值是null
(在原子操作中),它知道它必须执行实际的搜索。搜索后,它尝试再次将引用设置为 null
,使用 compareAndSet
和用于搜索的模式。因此,它只有在没有再次更改的情况下才能成功。否则,它将获取新值并重复。
这两个原子更新已经足以确保一次只有一个搜索操作,同时不会丢失更新的搜索模式。 doSearch
在检测到变化时及早 return 的能力非常好,调用者的循环不需要。
请注意,在此示例中,doSearch
中的检查已简化为引用比较(使用强制转换为 Object
以防止编译器警告),以证明它可以同样便宜作为您原始方法的 int
比较。只要没有设置新的字符串,引用都是一样的。
但是,实际上,您也可以使用字符串比较,即 if(!pattern.equals(patternRef.get())) { return false; }
而不会显着降低性能。 Java 中的字符串比较并不(必然)昂贵。 String
的 equals
的实现首先要做的是引用比较。所以如果字符串没有改变,它会立即return true
。否则,它将随后检查长度(与 C 字符串不同,长度是事先已知的)并在不匹配时立即 return false
。因此,在用户键入另一个字符或按退格键的典型情况下,长度会有所不同,比较会立即退出。
我正在尝试在 Java 中实现“键入时搜索”模式。
设计的目标是不会丢失任何更改,但与此同时,(耗时的)搜索操作应该能够提前中止并尝试更新的模式。
这是我到目前为止的结果(Java 8 伪代码):
AtomicReference<String> patternRef
AtomicLong modificationCount
ReentrantLock busy;
Consumer<List<ResultType>> resultConsumer;
// This is called in a background thread every time the user presses a key
void search(String pattern) {
// Update the pattern
synchronized {
patternRef.set(pattern)
modificationCount.inc()
}
try {
if (!busy.tryLock()) {
// Another search is already running, let it handle the change
return;
}
// Get local copy of the pattern and modCount
synchronized {
String patternCopy = patternRef.get();
long modCount = modificationCount.get()
}
while (true) {
// Try the search. It will return false when modificationCount changes before the search is finished
boolean success = doSearch(patternCopy, modCount)
if (success) {
// Search completed before modCount was changed again
break
}
// Try again with new pattern+modCount
synchronized {
patternCopy = patternRef.get();
modCount = modificationCount.get()
}
}
} finally {
busy.unlock();
}
}
boolean doSearch(String pattern, long modCount)
... search database ...
if (modCount != modificationCount.get()) {
return false;
}
... prepare results ...
if (modCount != modificationCount.get()) {
return false;
}
resultConsumer.accept(result); // Consumer for the UI code to do something
return modCount == modificationCount.get();
}
我是不是漏掉了重要的一点?竞争条件或类似情况?
Java8 中有什么东西可以使上面的代码更简单吗?
这段代码的根本问题可以概括为“试图通过多个不同的原子构造来实现原子性”。多个原子构造的组合不是原子的,尝试重新建立原子性会导致代码非常复杂、通常会损坏且效率低下。
在你的例子中,doSearch
的最后一次检查 modCount == modificationCount.get()
在仍然持有锁的情况下发生。在那之后,另一个线程(或多个其他线程)可以更新搜索字符串和 mod 计数,然后找到被占用的锁,因此,得出另一个搜索是 运行 的结论并会处理。
但是在最后一次 modCount == modificationCount.get()
检查之后那个线程并不关心。调用者只执行 if (success) { break; }
,然后是 finally { busy.unlock(); }
和 return。
所以答案是,是的,您有潜在的竞争条件。
因此,与其选择两个原子变量 synchronized
块和一个 ReentrantLock
,不如使用一个原子构造,例如单个原子变量:
final AtomicReference<String> patternRef = new AtomicReference<>();
Consumer<List<ResultType>> resultConsumer;
// This is called in a background thread every time the user presses a key
void search(String pattern) {
if(patternRef.getAndSet(pattern) != null) return;
// Try the search. doSearch will return false when not completed
while(!doSearch(pattern) || !patternRef.compareAndSet(pattern, null))
pattern = patternRef.get();
}
boolean doSearch(String pattern) {
//... search database ...
if(pattern != (Object)patternRef.get()) {
return false;
}
//... prepare results ...
if(pattern != (Object)patternRef.get()) {
return false;
}
resultConsumer.accept(result); // Consumer for the UI code to do something
return true;
}
这里,null
的值表示没有搜索是运行,所以如果后台线程将它设置为非null
值并找到旧值是null
(在原子操作中),它知道它必须执行实际的搜索。搜索后,它尝试再次将引用设置为 null
,使用 compareAndSet
和用于搜索的模式。因此,它只有在没有再次更改的情况下才能成功。否则,它将获取新值并重复。
这两个原子更新已经足以确保一次只有一个搜索操作,同时不会丢失更新的搜索模式。 doSearch
在检测到变化时及早 return 的能力非常好,调用者的循环不需要。
请注意,在此示例中,doSearch
中的检查已简化为引用比较(使用强制转换为 Object
以防止编译器警告),以证明它可以同样便宜作为您原始方法的 int
比较。只要没有设置新的字符串,引用都是一样的。
但是,实际上,您也可以使用字符串比较,即 if(!pattern.equals(patternRef.get())) { return false; }
而不会显着降低性能。 Java 中的字符串比较并不(必然)昂贵。 String
的 equals
的实现首先要做的是引用比较。所以如果字符串没有改变,它会立即return true
。否则,它将随后检查长度(与 C 字符串不同,长度是事先已知的)并在不匹配时立即 return false
。因此,在用户键入另一个字符或按退格键的典型情况下,长度会有所不同,比较会立即退出。