我计划在我的应用程序中使用此架构,但我不确定这是否安全。
为了给出一点背景知识,一堆服务器将计算属于单个任务的子任务的结果,并将它们报告回中央服务器。这段代码用于注册结果,还检查任务的所有子任务是否已完成,如果是,则仅报告该事实一次。
重要的是,所有任务必须在完成后立即报告一次(所有subTaskResults都已设置)。
有人可以帮忙吗?谢谢! (另外,如果你有更好的想法来解决这个问题,请告诉我!)
*请注意,为简洁起见,我简化了代码。
解决方案I
class Task {
//Populate with bunch of (Long, new AtomicReference()) pairs
//Actual app uses read only HashMap
Map<Id, AtomicReference<SubTaskResult>> subtasks = populatedMap();
Semaphore permission = new Semaphore(1);
public Task set(id, subTaskResult){
//null check omitted
subtasks.get(id).set(result);
return check() ? this : null;
}
private boolean check(){
for(AtomicReference ref : subtasks){
if(ref.get()==null){
return false;
}
}//for
return permission.tryAquire();
}
}//class
斯蒂芬C善意建议使用柜台。实际上,我曾经考虑过一次,但我推断JVM可以对操作进行重新排序,因此,在AtomicReference(由其他线程)设置结果之前,线程可以观察到递减的计数器(由另一个线程)。
*编辑:我现在看到这是线程安全的。我会选择这个解决方案。谢谢,斯蒂芬!
解决方案II
class Task {
//Populate with bunch of (Long, new AtomicReference()) pairs
//Actual app uses read only HashMap
Map<Id, AtomicReference<SubTaskResult>> subtasks = populatedMap();
AtomicInteger counter = new AtomicInteger(subtasks.size());
public Task set(id, subTaskResult){
//null check omitted
subtasks.get(id).set(result);
//In the actual app, if !compareAndSet(null, result) return null;
return check() ? this : null;
}
private boolean check(){
return counter.decrementAndGet() == 0;
}
}//class
答案 0 :(得分:3)
我假设您的用例是有多个多个线程正在调用set
,但对于id
的任何给定值,set
方法将仅调用一次。我还假设populateMap
为所有使用过的id
值创建条目,subtasks
和permission
真的是私有的。
如果是这样,我认为代码是线程安全的。
每个线程都应该看到subtasks
Map的初始化状态,包含所有键和所有AtomicReference引用。此状态永远不会更改,因此subtasks.get(id)
将始终提供正确的引用。 set(result)
调用在AtomicReference上运行,因此get()
中的后续check()
方法调用将在所有线程中提供最新的值....任何有多个线程调用检查的潜在比赛似乎都会自行解决。
然而,这是一个相当复杂的解决方案。一个更简单的解决方案是使用并发计数器;例如将Semaphore
替换为AtomicInteger
并使用decrementAndGet
,而不是重复扫描subtasks
中的check
地图。
在更新的解决方案中回应此评论:
实际上,我曾考虑过一次, 但我推断JVM可以 重新排序操作,因此,a 线程可以观察到递减 计数器(由另一个线程)之前 结果在AtomicReference中设置(通过 其他线程)。
根据定义,AtomicInteger和AtomicReference是原子的。任何尝试访问的线程都保证在访问时看到“当前”值。
在这种特殊情况下,每个线程在调用AtomicInteger上的set
之前调用相关AtomicReference 上的decrementAndGet
。这不能重新排序。线程执行的操作按顺序执行。由于这些是原子动作,因此其他线程也可以看到效果。
换句话说,它应该是线程安全的...... AFAIK。
答案 1 :(得分:0)
为AtomicReference.compareAndSet显式保证的原子性(每个类文档)扩展到set和get方法(每个包文档),因此在这方面,您的代码似乎是线程安全的。
但是,我不确定为什么你有Semaphore.tryAquire作为副作用,但没有免费代码来释放信号量,你的代码部分看起来是错误的。
答案 2 :(得分:0)
第二个解决方案确实提供了一个线程安全的锁存器,但是它很容易被调用set()
提供一个不在地图中的ID - 这将触发NullPointerException
- 或者超过使用相同的ID拨打set()
。后者会错误地递减计数器太多次并错误地报告完成时,可能有其他子任务ID没有提交结果。我的批评不是关于线程安全,而是关于不变的维护;即使没有与线程有关的问题,也会出现同样的缺陷。
解决这个问题的另一种方法是使用AbstractQueuedSynchronizer
,但它有点无偿:你可以实现一个精简计数信号量,每个调用set()
将调用releaseShared()
,减少通过旋转compareAndSetState()
计数器,tryAcquireShared()
只有在计数为零时才会成功。这或多或少是您在上面使用AtomicInteger
实现的,但是您将重用一个提供更多功能的工具,您可以将其用于设计的其他部分。
要充实基于AbstractQueuedSynchronizer
的解决方案,需要再添加一个操作来证明复杂性:能够等待所有子任务的结果返回,以便整个任务完成。在下面的代码中是Task#awaitCompletion()
和Task#awaitCompletion(long, TimeUnit)
。
同样,它可能有点过分,但为了讨论,我会分享它。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
final class Task
{
private static final class Sync extends AbstractQueuedSynchronizer
{
public Sync(int count)
{
setState(count);
}
@Override
protected int tryAcquireShared(int ignored)
{
return 0 == getState() ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(int ignored)
{
int current;
do
{
current = getState();
if (0 == current)
return true;
}
while (!compareAndSetState(current, current - 1));
return 1 == current;
}
}
public Task(int count)
{
if (count < 0)
throw new IllegalArgumentException();
sync_ = new Sync(count);
}
public boolean set(int id, Object result)
{
// Ensure that "id" refers to an incomplete task. Doing so requires
// additional synchronization over the structure mapping subtask
// identifiers to results.
// Store result somehow.
return sync_.releaseShared(1);
}
public void awaitCompletion()
throws InterruptedException
{
sync_.acquireSharedInterruptibly(0);
}
public void awaitCompletion(long time, TimeUnit unit)
throws InterruptedException
{
sync_.tryAcquireSharedNanos(0, unit.toNanos(time));
}
private final Sync sync_;
}
答案 3 :(得分:0)
我有一种奇怪的感觉,阅读你的示例程序,但这取决于程序的更大结构如何处理。同样检查完成的set函数几乎是代码味道。 :-)只是一些想法。
如果您与服务器进行同步通信,则可以使用具有相同线程数的ExecutorService,例如进行通信的服务器数量。从这里你得到一堆Futures,你可以自然地进行计算 - 获取调用将在需要结果时阻止但尚未存在。
如果您与服务器进行异步通信,则在将任务提交到服务器后,您也可以使用CountDownLatch。 await调用阻塞主线程直到所有子任务完成,其他线程可以接收结果并在每个接收到的结果上调用倒计时。
使用所有这些方法,除了在结构中并发存储结果是线程安全之外,您不需要特殊的线程安全措施。而且我敢打赌,还有更好的模式。