Java Threads: Basics and Modern Usage

Mihai PopescuMihai Popescu
4 min read

1. Basics of Threads

A thread in Java is a lightweight process that enables concurrent execution of code. It allows multiple tasks to run in parallel within a single application.

Key Concepts:

  • Thread Lifecycle: New → Runnable → Running → Terminated

  • Creating Threads:

    • By extending Thread and overriding run().

    • By implementing Runnable and passing it to a Thread instance.

    // Extending Thread
    class MyThread extends Thread {
        public void run() {
            System.out.println("Thread is running");
        }
    }

    // Using Runnable
    class MyRunnable implements Runnable {
        public void run() {
            System.out.println("Runnable thread is running");
        }
    }

    public class ThreadExample {
        public static void main(String[] args) {
            MyThread t1 = new MyThread();
            t1.start();

            Thread t2 = new Thread(new MyRunnable());
            t2.start();
        }
    }

2. Modern Approach to Threads

  1. Thread Pools (via ExecutorService) A thread pool manages a fixed number of reusable threads to execute tasks. This avoids the overhead of creating new threads for each task.

     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
    
     public class ThreadPoolExample {
         public static void main(String[] args) {
             ExecutorService executor = Executors.newFixedThreadPool(3);
    
             for (int i = 1; i <= 5; i++) {
                 int task = i;
                 executor.submit(() -> {
                     System.out.println("Executing Task " + task + " by " + Thread.currentThread().getName());
                 });
             }
    
             executor.shutdown();
         }
     }
    
    1. Virtual Threads (Project Loom) Introduced in Java 19 as a preview, virtual threads are lightweight threads designed for high scalability. They simplify concurrent programming by reducing the cost of thread creation.
    import java.util.concurrent.Executors;

    public class VirtualThreadExample {
        public static void main(String[] args) {
            try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
                for (int i = 1; i <= 5; i++) {
                    int task = i;
                    executor.submit(() -> {
                        System.out.println("Task " + task + " executed by " + Thread.currentThread());
                    });
                }
            }
        }
    }

3. Real-Life Example: Web Scraper

Imagine you need to scrape multiple web pages simultaneously.

    import java.net.HttpURLConnection;
    import java.net.URL;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    public class WebScraper {
        public static void main(String[] args) {
            String[] urls = {
                "https://example.com",
                "https://example.org",
                "https://example.net"
            };

            // Step 1: Create a Thread Pool with 3 threads
            //The fixed thread pool ensures there are exactly 3 reusable threads.
            // Each thread is capable of executing one task at a time.
            ExecutorService executor = Executors.newFixedThreadPool(3);

            // Step 2: Submit tasks (scraping each URL) to the thread pool
            for (String url : urls) {
                // The submit method places the tasks in a task queue. The thread pool assigns these tasks to the available threads
                executor.submit(() -> {
                    try {
                        // Step 3: Establish an HTTP connection to the URL
                        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
                        connection.setRequestMethod("GET");

                        // Step 4: Fetch the response code to ensure the page is reachable
                        int responseCode = connection.getResponseCode();

                        // Step 5: Print success message
                        System.out.println("Fetched " + url + " with response code: " + responseCode);
                    } catch (Exception e) {
                        // Step 6: Handle errors gracefully
                        System.err.println("Failed to fetch " + url + ": " + e.getMessage());
                    }
                });
            }

            // Step 7: Gracefully shut down the thread pool
            executor.shutdown();
        }
    }

Interaction Between Threads:

  • Each thread operates independently and handles one URL at a time.

  • Since the thread pool has 3 threads, up to 3 tasks can run concurrently. If more tasks are submitted, they wait in the queue until a thread becomes available.

Interaction Between Threads

  1. Concurrency:

  2. Independence:

    • Each thread works independently and does not share state or variables with other threads.

    • However, if shared resources (e.g., a shared file or list) were used, proper synchronization mechanisms (like synchronized blocks or Lock objects) would be needed to avoid conflicts.

  3. Task Queuing:

    • If there were more URLs than threads, the additional tasks would be queued. As threads complete their current tasks, they pick up the next task from the queue.
  4. Scalability:

    • This approach is scalable since the thread pool can be adjusted to handle more threads if needed.

Improvements & Extensions

  1. Handling Large Numbers of URLs:

    • If you have hundreds of URLs, consider increasing the thread pool size or using virtual threads to avoid exhausting system resources.
  2. Storing Results:

    • Instead of printing the response code, you could store it in a thread-safe collection like ConcurrentHashMap.
  3. Timeouts:

    • You can set timeouts for the HTTP connection to avoid threads hanging indefinitely:

        connection.setConnectTimeout(5000); // 5 seconds
        connection.setReadTimeout(5000);   // 5 seconds
      

Key Takeaways:

  • Basic Threads are simple but can lead to resource issues in large applications.

  • Thread Pools improve performance and resource management.

  • Virtual Threads provide a more scalable and modern approach to concurrency.

  • Using threads effectively requires understanding the task's nature and scalability needs.

0
Subscribe to my newsletter

Read articles from Mihai Popescu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mihai Popescu
Mihai Popescu