mercredi 1 juillet 2015

Null pointer exception from final variable within runnable

I have code that runs a portfolio of algorithms on a given problem, and then as soon as one algorithm finds the answer to a problem, the program continues. The other algorithms in the portfolio get voluntary signals to terminate, and the main thread of execution goes on.

One user of this code is sending me a stacktrace with a NullPointerException on the line "resultReference.set(solverResult);" As you can see from the code below, resultReference is a final variable and is initialized immediately. I don't see how it could possibly ever become null. I've spent a long time trying to reproduce the problem on my end to no avail. The line numbers on the user's stacktrace match up with the line numbers on my code. The user reports having seen the error on 3 different occasions, but infrequently (this does not happen every time a problem is solved), so maybe its some sort of race condition. This is with jdk 1.8_25.

Am I right in assuming that this error should be impossible because the variable is final? I'm not sure what to make of this stack trace and wanted some reassuring that it should be impossible.

public class ParallelSolver {

private final ListeningExecutorService executorService;
private final AtomicReference<Throwable> error;
private final List<Solver> solvers;
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ParallelSolver.class);

public ParallelSolver(int threadPoolSize, List<Solvers> solvers) {
    executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(threadPoolSize));
    error = new AtomicReference<>();
    this.solvers = solvers;
}

public SolverResult solve(Problem p) {
    final AtomicReference<SolverResult> resultReference = new AtomicReference<>();
    final List<Future> futures = new ArrayList<>();
    final Semaphore workDone = new Semaphore(0);
    try {
        // Submit one job per each solver in the portfolio
        solvers.forEach(solver -> {
            final ListenableFuture<Void> future = executorService.submit(() -> {
                SolverResult solverResult = solver.solve(p);
                if (solverResult.isConclusive()) {
                    log.debug("Signalling the blocked thread to wake up!");
                     // NPE HERE ON THIS LINE
                    resultReference.set(solverResult);
                    workDone.release(solvers.size());
                }
                log.debug("Releasing a single permit as the work for this thread is done.");
                workDone.release(1);
                log.debug("Job ending...");
                return null;
            });
            futures.add(future);
            Futures.addCallback(future, new FutureCallback<Void>() {
                @Override
                public void onSuccess(Void result) {

                }

                @Override
                public void onFailure(Throwable t) {
                    if (t instanceof CancellationException) {
                        return;
                    }
                    error.compareAndSet(null, t);
                    // Wake up the main thread (if it's still sleeping)
                    workDone.release(solvers.size());
                }
            });
        });
        // Wait for a thread to complete solving and signal you, or all threads to timeout
        log.debug("Main thread going to sleep");
        workDone.acquire(solvers.size());
        log.debug("Main thread waking up, checking for errors then cancelling futures");
        checkForErrors();
        // cancel any still to be launched futures
        futures.forEach(future -> future.cancel(false));
        log.debug("Returning now");
        return resultReference.get() == null ? SolverResult.createTimeoutResult() : resultReference.get();
    } catch (InterruptedException e) {
        throw new RuntimeException("Interrupted while running parallel job", e);
    }
}

/**
 * We want a fail-fast policy, but java executors aren't going to throw the exception on the main thread.
 * We can't call Future.get() and check for errors, because that might block.
 * So we set a variable when an error occurs, and check it here.
 */
private void checkForErrors() {
    if (error.get() != null) {
        log.error("Error occured while executing a task", error.get());
        throw new RuntimeException("Error occurred while executing a task", error.get());
    }
}

Aucun commentaire:

Enregistrer un commentaire