Как я могу убедиться, что метод вызывается только один раз несколькими потоками?

У меня есть следующая структура:

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

Метод someMethod может быть вызван одновременно многими потоками. doSomeProcessing может генерировать исключение (он использует некоторые данные в бэкэнд, которые могут стать устаревшими). ​​
Если исключение выбрано, то loadSomeHeavyData(); выполняет некоторую задачу времени, которая позволяет сказать "обновить" все текущие данные, и я могу позвонить doSomeProcessing();.
Проблема:. Как я могу убедиться, что loadSomeHeavyData(); вызывается только один раз? Если я помещаю некоторый атомный флаг в запись loadSomeHeavyData();, тогда я не могу быть уверен, когда это нужно очистить.
Как я могу это решить? Просто примечание: я не могу изменить doSomeProcessing();, поскольку это внешний API, и я использую шаблон декоратора, чтобы использовать его.

Ответ 1

В вашем методе 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 один раз, когда-либо - если нет, вам понадобится reset флаг и 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();
    }
}

Ответ 2

Использование ключевого слова synchronized:

public synchronized void someMethod(){  
    //doStuff
}

Заверяйте, что за один раз вводится только один поток.

Чтобы гарантировать, что метод вызывается только один раз, нет специальной функции языка; вы можете создать статическую переменную типа boolean, для которой первый поток вводит метод. При вызове метода всегда проверяйте этот флаг:

public class MyClass {
    private static boolean calledMyMethod;

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

Ответ 3

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. когда нет. запросов достигают threashold, блокировать выполнение 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;
    }
}

Ответ 4

Насколько я понимаю ваш вопрос, вам нужно загружать данные в непредсказуемый, но ограниченный промежуток времени. Существует три возможных способа: 1) Вы можете окружить свой вызов loadSomeHeavyData с помощью оператора if, который контролирует доступ к методу. 2) Вы можете изменить свой метод для обработки потока управления (решение об обновлении или нет) 3) Напишите поток обновлений и дайте ему сделать работу за вас Первые две альтернативы могут использовать внешнее логическое значение или генерировать логическое решение, используя timedelta между последним вызовом и текущим временем вызова. Третьей альтернативой может быть временная нить, которая выполняется каждые n секунд/минут и загружает тяжелые данные.

Ответ 5

Мы написали библиотеку, которая включает утилиту, ленивую загрузку/вызов метода. Он гарантирует единственную семантику использования и сохраняет любые заброшенные исключения, как вы ожидали.

Использование прост:

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() и дождитесь завершения потока производителя (первого вызывающего абонента).