Four Styles of Java Concurrency: Threads, Executors, ForkJoin, and Actors
This article compares four Java concurrency approaches—raw threads, the Executor framework, parallel streams backed by ForkJoinPool, and the Actor model—explaining their implementations, advantages, drawbacks, and typical pitfalls through concrete code examples and practical discussion.
We live in a world where many things happen concurrently, and Java programs can exploit multiple processors to run tasks in parallel. However, writing correct concurrent code is difficult, so developers rely on established models that emphasize different aspects of the problem.
The article demonstrates four ways to achieve concurrency in Java, using the same sample task (fetching the first successful result from a list of search‑engine URLs) to keep the examples comparable.
Method 1: Raw Threads – Directly create a Thread for each engine, store the first result in an AtomicReference , and busy‑wait until a value appears.
private static String getFirstResult(String question, List<String> engines) {
AtomicReference<String> result = new AtomicReference<>();
for(String base: engines) {
String url = base + question;
new Thread(() -> {
result.compareAndSet(null, WS.url(url).get());
}).start();
}
while(result.get() == null); // wait for some result to appear
return result.get();
}Raw threads give the most direct mapping to OS threads but require manual lifecycle management and can lead to resource exhaustion if many threads are created.
Method 2: Executors and CompletionService – Use the Executor API to manage a fixed pool of worker threads and retrieve the first completed task via ExecutorCompletionService .
private static String getFirstResultExecutors(String question, List<String> engines) {
ExecutorCompletionService<String> service = new ExecutorCompletionService<String>(Executors.newFixedThreadPool(4));
for(String base: engines) {
String url = base + question;
service.submit(() -> {
return WS.url(url).get();
});
}
try {
return service.take().get();
} catch(InterruptedException | ExecutionException e) {
return null;
}
}This approach abstracts thread creation, limits concurrency, and provides a clean way to obtain the first result while handling queueing and lifecycle concerns.
Method 3: Parallel Streams (ForkJoinPool) – Java 8’s parallel streams internally use ForkJoinPool.commonPool() to process elements concurrently.
private static String getFirstResult(String question, List<String> engines) {
// get element as soon as it is available
Optional<String> result = engines.stream().parallel().map((base) -> {
String url = base + question;
return WS.url(url).get();
}).findAny();
return result.get();
}Parallel streams simplify code but give less control over thread usage and may hide performance characteristics, especially when the data source is unknown.
Method 4: Actor Model (Akka) – Actors encapsulate state and communicate via messages, avoiding shared mutable data. The example uses Akka’s UntypedActor to fetch URLs concurrently.
static class Message {
String url;
Message(String url) {this.url = url;}
}
static class Result {
String html;
Result(String html) {this.html = html;}
}
static class UrlFetcher extends UntypedActor {
@Override
public void onReceive(Object message) throws Exception {
if (message instanceof Message) {
Message work = (Message) message;
String result = WS.url(work.url).get();
getSender().tell(new Result(result), getSelf());
} else {
unhandled(message);
}
}
}
static class Querier extends UntypedActor {
private String question;
private List<String> engines;
private AtomicReference<String> result;
public Querier(String question, List<String> engines, AtomicReference<String> result) {
this.question = question;
this.engines = engines;
this.result = result;
}
@Override public void onReceive(Object message) throws Exception {
if(message instanceof Result) {
result.compareAndSet(null, ((Result) message).html);
getContext().stop(self());
} else {
for(String base: engines) {
String url = base + question;
ActorRef fetcher = this.getContext().actorOf(Props.create(UrlFetcher.class), "fetcher-"+base.hashCode());
Message m = new Message(url);
fetcher.tell(m, self());
}
}
}
}
private static String getFirstResultActors(String question, List<String> engines) {
ActorSystem system = ActorSystem.create("Search");
AtomicReference<String> result = new AtomicReference<>();
final ActorRef q = system.actorOf(
Props.create((UntypedActorFactory) () -> new Querier(question, engines, result)), "master");
q.tell(new Object(), ActorRef.noSender());
while(result.get() == null);
return result.get();
}The actor model provides high scalability and fault tolerance but requires careful design to avoid global state and can increase architectural complexity.
In the concluding section, the author invites readers to share which concurrency style they use most and reflects on the trade‑offs among simplicity, configurability, performance, and suitability for large systems.
Overall, the article educates developers on four distinct Java concurrency techniques, illustrating each with runnable code, discussing their pros and cons, and helping readers choose the right tool for their specific problem.
Qunar Tech Salon
Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.