如何确保一个方法只被多个线程调用一次?

12

我有以下结构:

public void someMethod(){  
   //DO SOME STUFF
   try{  
    doSomeProcessing();  
   }  
   catch (Exception e){  
        loadSomeHeavyData();  
        doSomeProcessing();      
   }    
}  

方法someMethod可以被多个线程同时调用,doSomeProcessing可能会抛出异常(它正在使用一些后端数据,这些数据可能会变得过时)。 如果抛出异常,则loadSomeHeavyData();执行一些耗时的任务,例如“更新”所有当前数据,并且我可以调用doSomeProcessing();问题:如何确保loadSomeHeavyData();只被调用一次?如果在loadSomeHeavyData();入口处放置原子标志,则无法确定何时应清除该标志。 怎么解决?请注意:由于doSomeProcessing();是外部API,我正在使用修饰器模式来使用它,因此无法修改它。


1
只要一次,还是避免并发调用? - Jon Skeet
我怎样才能确保只调用一次loadSomeHeavyData()函数?不是很清楚,您是想说如果一个线程已经调用了loadSomeHeavyData(),其他线程即使发生异常也不应该再调用它吗? - Azodious
@Azodious:要么就是其他线程“理解”重 API 已经被调用,并且第一个线程将为它们刷新数据。 - Jim
@Jim:但是如果异常再次抛出,你希望发生什么?准确捕获所有要求非常重要。 - Jon Skeet
@Jim:在catch中调用loadSomeHeavyData的位置,改为将请求放入队列中调用。这是一种设计变更,但可以更好地满足您的需求。请查看我下面的答案。 - Azodious
显示剩余5条评论
5个回答

13

您的loadSomeHeavyData方法可以使用阻塞机制,使所有线程等待直到它完成其更新,但只允许其中一个实际执行更新:

private final AtomicBoolean updateStarted = new AtomicBoolean();
private final CountDownLatch updateFinished = new CountDownLatch(1);

public void loadSomeHeavyData() {
    if (updateStarted.compareAndSet(false, true)) {
        //do the loading
        updateFinished.countDown();
    } else {
        //update already running, wait
        updateFinished.await();
    }
}

请注意我的假设:

  • 您希望所有线程等待直到加载完成,以便它们可以使用更新后的数据再次调用doSomeProcessing
  • 您只调用loadSomeHeavyData一次 - 如果不是这样,您将需要重置标志和CountdownLatch(这可能不是最合适的机制)。

编辑

您最新的评论表明,实际上您想多次调用loadSomeHeavyData,只是不能同时多次调用。

private final Semaphore updatePermit = new Semaphore(1);

public void loadSomeHeavyData() {
    if (updatePermit.tryAcquire()) {
        //do the loading and release updatePermit when done
        updatePermit.release();
    } else {
        //update already running, wait
        updatePermit.acquire();
        //release the permit immediately
        updatePermit.release();
    }
}

啊!如果以后出现异常,我需要再次调用loadSomeHeavyData。我该如何在这种情况下使用您的答案? - Jim
@Jim 看到我的编辑了吗?你应该更新你的问题,让它更具体明确。 - assylias
@Jim 一个技巧是使用AtomicBoolean,并在一定时间后将其重置为false。如果它为true,则跳过该方法。 - assylias
但现在问题不是取决于时间间隔吗?它应该太小了吗?太大了吗?有多大? - Jim
1
对于获取许可加载数据的线程,如果在加载数据时抛出异常,会导致许可未被释放吗?或许可以在finally块中使用try finally来释放许可。 - Mingjiang Shi
显示剩余5条评论

4

使用 synchronized 关键字:

public synchronized void someMethod(){  
    //doStuff
}

您要确保只有一个线程进入。

为了确保该方法仅被调用一次,没有特殊的语言特性;您可以创建一个静态变量,类型为布尔型,并由首个进入该方法的线程将其设置为true。在调用该方法时总是检查该标志:

public class MyClass {
    private static boolean calledMyMethod;

    public synchronized void someMethod() {
        if(calledMyMethod) { 
            return;
        } else {
            calledMyMethod = true;
            //method logic
        }           
    }
} 

很遗憾,我无法将其更改为“同步”。 - Jim

1
public void someMethod()
{  
    //DO SOME STUFF
    try
    {  
        doSomeProcessing();  
    }   
    catch (Exception e)
    {   
        loadSomeHeavyData();  // Don't call here but add a request to call in a queue.
                              // OR update a counter
        doSomeProcessing();      
    }
}

其中一个解决方案可以是创建一个队列,每个线程将其调用loadSomeHeavyData的请求放入队列中。当请求数量达到阈值时,阻塞someMethod的执行并调用loadSomeHeavyData来清除队列。

伪代码可能如下:

int noOfrequests = 0;
public void someMethod()
{
    // block incoming threads here.  
    while(isRefreshHappening);

    //DO SOME STUFF
    try
    { 
        doSomeProcessing();  
    }   
    catch (Exception e)
    {
        // Log the exception
        noOfrequests++;   
    }
}

// Will be run by only one thread
public void RefreshData()
{
    if(noOfrequests >= THRESHOLD)
    {
        isRefreshHappening = true;
        // WAIT if any thread is present in try block of somemethod
        // ...
        loadSomeHeavyData();
        noOfrequests = 0;
        isRefreshHappening = false;
    }
}

你可以添加一些小片段来说明你的建议吗? - Jim

0
我们编写了一个库,其中包括一个实用工具,可以懒加载/调用方法。它保证单一使用语义,并保留任何抛出的异常,正如您所期望的那样。
使用非常简单:
LazyReference<Thing> heavyThing = new LazyReference<Thing>() {
  protected Thing create() {
    return loadSomeHeavyData();
  }
};

public void someMethod(){  
  //DO SOME STUFF
  try{  
    doSomeProcessing();  
  }  
  catch (Exception e){  
    heavyThing.get();  
    doSomeProcessing();      
  }    
}  

所有线程都在get()上阻塞,并等待生产者线程(第一个调用者)完成。


似乎有很多依赖关系。我该如何使用它? - Jim
这似乎可以满足我的需求,但它似乎需要许多库(如Google等)。 - Jim
它有一个依赖于谷歌的guava - 我们发现每个项目已经依赖于此,而我们想使用一致的Function接口。最近,我们添加了一个Promise,它也使用了guava的ListenableFuture。所有其他依赖项都是测试(或在查找错误注释的情况下是可选的)。 - Jed Wesley-Smith

0
据我理解,您需要在不可预测但有限的时间间隔内加载数据。有三种可能的方法可以实现: 1)您可以将对loadSomeHeavyData的调用包围在一个if语句中,该语句控制对该方法的访问。 2)您可以更改方法以处理控制流程(决定是否更新) 3)编写一个更新线程,并让它为您完成工作 前两种替代方案可以使用外部布尔值或通过使用上次调用和当前调用时间之间的时间差生成布尔决策。 第三种替代方案是一个定时线程,每n秒/分钟运行一次并加载重要数据。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接