这个java代码是线程安全的吗?

时间:2009-12-19 00:12:59

标签: java concurrency

我计划在我的应用程序中使用此架构,但我不确定这是否安全。

为了给出一点背景知识,一堆服务器将计算属于单个任务的子任务的结果,并将它们报告回中央服务器。这段代码用于注册结果,还检查任务的所有子任务是否已完成,如果是,则仅报告该事实一次。

重要的是,所有任务必须在完成后立即报告一次(所有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

4 个答案:

答案 0 :(得分:3)

我假设您的用例是有多个多个线程正在调用set,但对于id的任何给定值,set方法将仅调用一次。我还假设populateMap为所有使用过的id值创建条目,subtaskspermission真的是私有的。

如果是这样,我认为代码是线程安全的。

每个线程都应该看到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调用阻塞主线程直到所有子任务完成,其他线程可以接收结果并在每个接收到的结果上调用倒计时。

使用所有这些方法,除了在结构中并发存储结果是线程安全之外,您不需要特殊的线程安全措施。而且我敢打赌,还有更好的模式。