Java 8 Threading and Executor Services
Delve into the concepts of threading and executor services in Java 8, exploring their various types and providing detailed examples to illustrate their use.
Join the DZone community and get the full member experience.
Join For FreeUnderstanding Java 8 Threading and Executor Services
Java 8 introduced several enhancements to its concurrency framework, primarily through the java.util.concurrent
package. This has made it easier to manage multiple threads and execute tasks concurrently. In this article, we will delve into the concepts of threading and executor services in Java 8, exploring their various types and providing detailed examples to illustrate their use.
Introduction to Java Threading
Threading is the backbone of concurrent programming in Java. A thread is a lightweight process that allows multiple tasks to be executed simultaneously. Java provides two primary ways to create a thread:
Extending the Thread
Class
By extending the Thread
class and overriding its run
method, you can create a new thread.
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
thread1.start(); // Starts the new thread, calling the run method
}
}
Implementing the Runnable
Interface
This method is more flexible, as it allows you to extend another class while still providing a thread's run
method.
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable is running");
}
}
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable());
thread1.start();
}
}
The Executor framework in Java provides a higher-level alternative for managing threads. It decouples task submission from task execution, allowing for more flexible and efficient task management.
Key Components of the Executor Framework
Executor
The Executor
interface provides a method to submit a Runnable
task for execution.
Executor executor = new Executor() {
public void execute(Runnable r) {
new Thread(r).start();
}
};
executor.execute(() -> System.out.println("Task executed"));
ExecutorService
ExecutorService
extends Executor
and adds methods for managing the lifecycle of tasks and the executor itself.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
System.out.println("Task executed by " + Thread.currentThread().getName());
});
}
executorService.shutdown();
}
}
ScheduledExecutorService
This interface extends ExecutorService
and supports scheduling tasks to run after a delay or periodically.
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.schedule(() -> {
System.out.println("Task executed after a delay");
}, 5, TimeUnit.SECONDS);
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("Periodic task executed");
}, 0, 3, TimeUnit.SECONDS);
scheduledExecutorService.scheduleWithFixedDelay(() -> {
System.out.println("Task executed with fixed delay");
}, 0, 3, TimeUnit.SECONDS);
}
}
Java provides various types of executors to cater to different concurrency needs. Let's explore each of them with examples.
Fixed Thread Pool
A fixed thread pool contains a fixed number of threads. This is useful when you have a consistent number of tasks that need to be executed concurrently.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
final int index = i;
fixedThreadPool.submit(() -> {
System.out.println("Task " + index + " executed by " + Thread.currentThread().getName());
});
}
fixedThreadPool.shutdown();
}
}
Cached Thread Pool
A cached thread pool creates new threads as needed but will reuse previously constructed threads when they are available. This is ideal for applications that perform many short-lived asynchronous tasks.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
final int index = i;
cachedThreadPool.submit(() -> {
System.out.println("Task " + index + " executed by " + Thread.currentThread().getName());
});
}
cachedThreadPool.shutdown();
}
}
Single Thread Executor
A single-thread executor uses a single worker thread to execute tasks sequentially. It guarantees that tasks will be executed in the order they are submitted.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
final int index = i;
singleThreadExecutor.submit(() -> {
System.out.println("Task " + index + " executed by " + Thread.currentThread().getName());
});
}
singleThreadExecutor.shutdown();
}
}
Scheduled Thread Pool
A scheduled thread pool allows you to schedule tasks to run after a delay or periodically.
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
// Schedule a task to run after a delay
scheduledThreadPool.schedule(() -> {
System.out.println("Task executed after a delay");
}, 5, TimeUnit.SECONDS);
// Schedule a task to run periodically
scheduledThreadPool.scheduleAtFixedRate(() -> {
System.out.println("Periodic task executed");
}, 0, 3, TimeUnit.SECONDS);
// Schedule a task to run with a fixed delay between the end of one execution and the start of the next
scheduledThreadPool.scheduleWithFixedDelay(() -> {
System.out.println("Task executed with fixed delay");
}, 0, 3, TimeUnit.SECONDS);
}
}
Work Stealing Pool
The work-stealing pool uses a ForkJoinPool
to balance the load across available processors, which is ideal for parallel task execution.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WorkStealingPoolExample {
public static void main(String[] args) {
ExecutorService workStealingPool = Executors.newWorkStealingPool();
for (int i = 0; i < 10; i++) {
final int index = i;
workStealingPool.submit(() -> {
System.out.println("Task " + index + " executed by " + Thread.currentThread().getName());
});
}
workStealingPool.shutdown();
}
}
Single Thread Scheduled Executor
A single-thread scheduled executor schedules tasks with a single thread, similar to the scheduled thread pool but with only one thread.
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class SingleThreadScheduledExecutorExample {
public static void main(String[] args) {
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
// Schedule a task to run after a delay
singleThreadScheduledExecutor.schedule(() -> {
System.out.println("Task executed after a delay");
}, 5, TimeUnit.SECONDS);
// Schedule a task to run periodically
singleThreadScheduledExecutor.scheduleAtFixedRate(() -> {
System.out.println("Periodic task executed");
}, 0, 3, TimeUnit.SECONDS);
// Schedule a task to run with a fixed delay between the end of one execution and the start of the next
singleThreadScheduledExecutor.scheduleWithFixedDelay(() -> {
System.out.println("Task executed with fixed delay");
}, 0, 3, TimeUnit.SECONDS);
}
}
Handling Callable Tasks
Callable
tasks can return a result and throw a checked exception. The result can be obtained using a Future
.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableTaskExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Callable<String> callableTask = () -> {
return "Callable result";
};
Future<String> future = executorService.submit(callableTask);
try {
String result = future.get();
System.out.println("Callable task result: " + result);
} catch (Exception e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
Conclusion
Java 8's threading and executor services offer a powerful and flexible framework for concurrent programming. By understanding and leveraging the different types of executors, developers can optimize their applications for performance and scalability. Whether you are dealing with short-lived asynchronous tasks or need to schedule tasks periodically, the java.util.concurrent
package provides the tools you need to manage threads effectively.
Opinions expressed by DZone contributors are their own.
Comments