Questão
Como você cria um carregador de plano de fundo adequado no Java 8? As condições:
- os dados devem ser carregados em segundo plano
- após o carregamento, os dados devem ser exibidos
- enquanto os dados são carregados, nenhuma solicitação adicional deve ser aceita
- se houver solicitações enquanto os dados foram carregados, outro carregamento deve ser agendado após um certo tempo limite (por exemplo, 5 segundos)
O objetivo é, por exemplo, ter solicitações de recarga aceitas, mas não o banco de dados inundado com as solicitações.
MCVE
Aqui está um MCVE. Consiste em uma tarefa em segundo plano que simula o carregamento simplesmente invocando Thread.sleep por 2 segundos. A tarefa é agendada a cada segundo, o que naturalmente leva a uma sobreposição das tarefas de carregamento em segundo plano, o que deve ser evitado.
public class LoadInBackgroundExample {
/**
* A simple background task which should perform the data loading operation. In this minimal example it simply invokes Thread.sleep
*/
public static class BackgroundTask implements Runnable {
private int id;
public BackgroundTask(int id) {
this.id = id;
}
/**
* Sleep for a given amount of time to simulate loading.
*/
@Override
public void run() {
try {
System.out.println("Start #" + id + ": " + Thread.currentThread());
long sleepTime = 2000;
Thread.sleep( sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Finish #" + id + ": " + Thread.currentThread());
}
}
}
/**
* CompletableFuture which simulates loading and showing data.
* @param taskId Identifier of the current task
*/
public static void loadInBackground( int taskId) {
// create the loading task
BackgroundTask backgroundTask = new BackgroundTask( taskId);
// "load" the data asynchronously
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
CompletableFuture<Void> future = CompletableFuture.runAsync(backgroundTask);
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return "task " + backgroundTask.id;
}
});
// display the data after they are loaded
CompletableFuture<Void> future = completableFuture.thenAccept(x -> {
System.out.println( "Background task finished:" + x);
});
}
public static void main(String[] args) {
// runnable which invokes the background loader every second
Runnable trigger = new Runnable() {
int taskId = 0;
public void run() {
loadInBackground( taskId++);
}
};
// create scheduler
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
ScheduledFuture<?> beeperHandle = scheduler.scheduleAtFixedRate(trigger, 0, 1, TimeUnit.SECONDS);
// cancel the scheudler and the application after 10 seconds
scheduler.schedule(() -> beeperHandle.cancel(true), 10, TimeUnit.SECONDS);
try {
beeperHandle.get();
} catch (Throwable th) {
}
System.out.println( "Cancelled");
System.exit(0);
}
}
A saída é esta:
Start #0: Thread[ForkJoinPool.commonPool-worker-2,5,main]
Start #1: Thread[ForkJoinPool.commonPool-worker-4,5,main]
Start #2: Thread[ForkJoinPool.commonPool-worker-6,5,main]
Finish #0: Thread[ForkJoinPool.commonPool-worker-2,5,main]
Background task finished:task 0
Finish #1: Thread[ForkJoinPool.commonPool-worker-4,5,main]
Background task finished:task 1
Start #3: Thread[ForkJoinPool.commonPool-worker-4,5,main]
Finish #2: Thread[ForkJoinPool.commonPool-worker-6,5,main]
Background task finished:task 2
Start #4: Thread[ForkJoinPool.commonPool-worker-6,5,main]
Start #5: Thread[ForkJoinPool.commonPool-worker-2,5,main]
Finish #3: Thread[ForkJoinPool.commonPool-worker-4,5,main]
Background task finished:task 3
Start #6: Thread[ForkJoinPool.commonPool-worker-4,5,main]
Finish #4: Thread[ForkJoinPool.commonPool-worker-6,5,main]
Background task finished:task 4
Finish #5: Thread[ForkJoinPool.commonPool-worker-2,5,main]
Background task finished:task 5
Start #7: Thread[ForkJoinPool.commonPool-worker-2,5,main]
Finish #6: Thread[ForkJoinPool.commonPool-worker-4,5,main]
Start #8: Thread[ForkJoinPool.commonPool-worker-6,5,main]
Background task finished:task 6
Start #9: Thread[ForkJoinPool.commonPool-worker-4,5,main]
Finish #7: Thread[ForkJoinPool.commonPool-worker-2,5,main]
Background task finished:task 7
Start #10: Thread[ForkJoinPool.commonPool-worker-2,5,main]
Finish #8: Thread[ForkJoinPool.commonPool-worker-6,5,main]
Background task finished:task 8
Cancelled
O objetivo é ter, por exemplo, o número 1 e o número 2 ignorados, porque o número 0 ainda está sendo executado.
Problema
Onde você define adequadamente o mecanismo de bloqueio? A sincronização deve ser usada? Ou algum AtomicBoolean
? E se sim, deveria estar dentro do get()
método ou em outro lugar?
fonte
ExecutorService
com um tamanho de pool de threads igual a 1?BlockingQueue
?Respostas:
Você já possui um pool de threads para executar a tarefa. Não é necessariamente e dificulta a execução da tarefa em outro executor assíncrono (
ForkJoinPool
quando você usaCompletableFuture
)Simplifique:
O ScheduledExecutorService garantirá que apenas uma tarefa seja executada por vez quando você a invocou com scheduleAtFixedRate
fonte
Tomando o seguinte como os requisitos:
A solução pode ser criada com base em
Executors.newSingleThreadExecutor()
,CompletableFuture
eLinkedBlockingQueue
:Após a execução, o stdout terá a seguinte saída:
fonte
Eu adicionei um AtomicInteger que atuará como um contador para executar tarefas com métodos simples lock () e unlock () com essa pequena alteração no seu código original.
Aqui está a minha solução para sua tarefa:
ATUALIZAR
Alterei os métodos lock () e unlock () para um formato mais simples:
fonte
Se você entender, você tem várias tarefas em segundo plano ao mesmo tempo. Como essas tarefas estão realizando exatamente o mesmo trabalho que você não deseja executá-las em paralelo, você precisa de uma tarefa para concluir o trabalho e compartilhar seus resultados com outras pessoas. Portanto, se você obtiver 10
CompletableFuture
simultaneamente, deseja que um deles chame 'recarregar' no db e compartilhe os resultados da execução com outras pessoas de uma maneira que tudoCompletableFuture
seja concluído normalmente com o resultado. Eu assumo isso dee
se minhas suposições estão corretas, você pode tentar minha solução.
Eu tenho algum tipo de relacionamento pai-filho entre tarefas. A tarefa dos pais é aquela que realmente está fazendo seu trabalho e compartilha resultados obtidos com seus filhos. tarefa filho é uma tarefa que foi adicionada enquanto a tarefa pai ainda estava em execução; a tarefa filho aguarda até que a tarefa pai termine sua execução. Como os resultados da tarefa dos pais ainda são "frescos", eles são copiados para cada filho e todos eles completam seu futuro.
E aqui está a saída:
fonte
se você quiser apenas um único thread de acesso, um simples sincronizado faria o trabalho ...
resultado:
código:
fonte
Eu tentei uma solução usando um switch duplo Thread, veja a classe
BackgroundTaskDualSwitch
, ele simula o carregamento usando oCompletableFuture
. A idéia é deixar uma segunda tarefa aguardar até que a tarefa atualmente em execução seja concluídaBackgroundTask
. Isso garante que o máximo de um tarefa Thread esteja em execução e o máximo de uma tarefa Thread esteja aguardando. Solicitações adicionais são ignoradas até que a tarefa em execução seja concluída e fique 'livre' para lidar com a próxima solicitação.A saída é:
fonte
O primeiro thread que começa a fazer o trabalho caro notificará com um retorno de chamada o resultado. Outros threads que tentarem executá-lo serão registrados no ExpensiveWork.notificables; assim, quando o trabalho caro terminar, o thread que fez o trabalho os notificará.
Enquanto isso, os threads estão verificando o resultado a cada 5 segundos.
E esta é a saída:
fonte