How do I safely share data between threads with ConcurrentHashMap?

When working with a multithreaded application, ConcurrentHashMap is a great choice for safely sharing data between threads. It is a thread-safe version of a HashMap that provides high concurrency for both retrieval and updates. Here are some guidelines to safely use a ConcurrentHashMap in a multithreaded environment:


1. Use Thread-Safe Access Operations

ConcurrentHashMap ensures that operations like put(), get(), remove(), containsKey() are thread-safe. Unlike HashMap, you can safely use these methods concurrently across multiple threads without additional synchronization.

package org.kodejava.util.concurrent;

import java.util.concurrent.ConcurrentHashMap;

public class ExampleConcurrentHashMap {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key1", 1);

        // Reading and updating the map from multiple threads
        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName());
            Integer value = map.get("key1");
            if (value != null) {
                map.put("key1", value + 1);
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
    }
}

This code works safely across threads because the put() and get() operations are thread-safe.


2. Avoid Compound Operations

While individual operations like put() and get() are thread-safe, compound operations (operations that consist of multiple actions, e.g., check-then-act) are not atomic by default. For example, the following code might fail in a multithreaded scenario:

if (!map.containsKey("key")) {  // Thread 1 might pass this check
    map.put("key", 42);         // Thread 2 might also pass this check before Thread 1 puts the value
}

To perform compound operations atomically, use methods provided by ConcurrentHashMap, such as putIfAbsent(), compute(), or merge().

Example: Use putIfAbsent

map.putIfAbsent("key", 42); // Ensures that "key" is inserted only if it isn't already present

Example: Use compute

map.compute("key", (k, v) -> (v == null) ? 1 : v + 1);
// Safely updates the value of "key" atomically

Example: Use merge

map.merge("key", 1, Integer::sum);
// Combines a new value with the existing value of "key" in a thread-safe manner

3. Leverage Concurrent Iteration

ConcurrentHashMap allows thread-safe iteration over its entries using iterators. However, note that the iterator reflects the state of the map at the moment it was created. Any changes made to the map by other threads after the iterator creation will not throw ConcurrentModificationException, but they may or may not be seen during iteration.

Safe Iteration Example

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);

map.forEach((key, value) -> {
    System.out.println(key + ": " + value);
});

Iterating and updating simultaneously can still be done safely through operations like compute() or computeIfPresent() within the iteration.


4. Understand Default Concurrency Level

ConcurrentHashMap partitions the map into segments internally to reduce contention among threads. You can adjust the level of concurrency (number of segments) by specifying it during construction, but the default value is sufficient for most use cases.

Custom Concurrency Level Example:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(16, 0.75f, 32);
// 32 is the concurrency level (number of threads allowed to modify without contention)

5. Use Bulk Operations for Performance

ConcurrentHashMap includes bulk operations like forEach(), reduce(), and search(). These operations are implemented to efficiently work with large volumes of data in a concurrent environment.

Example: Use forEach

map.forEach(1, (key, value) -> {
    System.out.println(key + ": " + value);
});
// The first parameter is parallelismThreshold (minimum size to make it parallelizable)

Example: Use reduce

Integer sum = map.reduceValues(1, Integer::sum);
System.out.println("Sum of all values: " + sum);

6. Avoid Manual Synchronization

Avoid adding explicit locks like synchronized or ReentrantLock with ConcurrentHashMap, as this can lead to deadlocks or significantly hinder performance. Instead, rely on the built-in atomic methods provided by the class.


7. Be Aware of Null Restrictions

Unlike HashMap, ConcurrentHashMap does not support null keys or null values. If you try to use null, it will throw a NullPointerException. Use valid non-null keys and values at all times.


Conclusion

ConcurrentHashMap is a powerful and flexible tool for managing shared data across multiple threads. To use it safely and efficiently:

  1. Use atomic methods like putIfAbsent, compute, or merge for compound operations.
  2. Avoid manual synchronization.
  3. Leverage bulk operations for large datasets.
  4. Handle data consistently without assuming atomicity for compound actions unless explicitly supported by the API.

By following these guidelines, you can minimize race conditions and improve the safety and performance of your multithreaded application.

How do I schedule tasks using ScheduledExecutorService?

The ScheduledExecutorService is a Java concurrency utility used for scheduling tasks to run after a delay or to execute periodically. Introduced in Java 5 as part of the java.util.concurrent package, it provides flexible scheduling functionality.
Here’s how you can use it:

1. Getting an Instance of ScheduledExecutorService

You can obtain an instance using the Executors factory class:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

// Single-threaded scheduled executor
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);  

2. Methods to Schedule Tasks

A) Schedule a Task with a Delay

To schedule a task to execute once after a specified delay:

import java.util.concurrent.TimeUnit;

scheduler.schedule(() -> {
    System.out.println("Task executed after delay");
}, 5, TimeUnit.SECONDS);

In this example:

  • A task will run after a delay of 5 seconds.

B) Schedule a Task at Fixed Rate

To schedule a task to run repeatedly at a fixed rate, starting after an initial delay:

scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Task executed at fixed rate");
}, 2, 3, TimeUnit.SECONDS);

In this example:

  • The task will first execute 2 seconds after scheduling.
  • Subsequent executions will occur every 3 seconds, irrespective of the previous task’s runtime.

C) Schedule a Task with Fixed Delay

To schedule a task to run repeatedly with a fixed delay between the completion of one execution and the start of the next:

scheduler.scheduleWithFixedDelay(() -> {
    System.out.println("Task executed with fixed delay");
}, 2, 3, TimeUnit.SECONDS);

In this example:

  • The task will first execute 2 seconds after scheduling.
  • Subsequent executions will occur 3 seconds after the previous task finishes.

3. Shutting Down the Scheduler

It’s important to properly shut down the scheduler to release resources when it is no longer needed:

scheduler.shutdown();

If you want to wait for currently executing tasks to finish before termination:

try {
    if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
        scheduler.shutdownNow();  // Forcefully shutdown if tasks don't complete within 60 seconds
    }
} catch (InterruptedException e) {
    scheduler.shutdownNow();
}

4. Example: Complete Program

Here’s a complete program demonstrating all of the above:

package org.kodejava.util.concurrent;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorExample {
    public static void main(String[] args) {
        // Create a ScheduledExecutorService with a single thread
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        // Schedule a task to run after a delay
        scheduler.schedule(() -> System.out.println("Task1 executed after 5 seconds"), 5, TimeUnit.SECONDS);

        // Schedule a task to run periodically at a fixed rate
        scheduler.scheduleAtFixedRate(() -> System.out.println("Task2 executed at fixed rate"), 2, 3, TimeUnit.SECONDS);

        // Schedule a task to run periodically with a fixed delay
        scheduler.scheduleWithFixedDelay(() -> System.out.println("Task3 executed with fixed delay"), 2, 5, TimeUnit.SECONDS);

        // Shut down the scheduler after some time for demonstration
        scheduler.schedule(() -> {
            System.out.println("Shutting down scheduler...");
            scheduler.shutdown();
        }, 20, TimeUnit.SECONDS);
    }
}

Key Points to Remember

  • Use scheduleAtFixedRate for periodic tasks that need to run at a consistent interval irrespective of the task runtime.
  • Use scheduleWithFixedDelay when the delay between task executions must consider the runtime of the previous task.
  • Always shut down the ScheduledExecutorService to release resources.

How do I create a thread pool with Executors in Java?

In Java, the java.util.concurrent.Executors class provides factory methods for creating and managing thread pools easily. Below are common ways to create a thread pool using Executors:

1. Fixed Thread Pool

A fixed thread pool contains a fixed number of threads. This is useful when you have a specific number of tasks to manage and want to limit the number of concurrently running threads.

package org.kodejava.util.concurrent;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        // Create a fixed thread pool with 3 threads
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            fixedThreadPool.execute(() -> {
                System.out.println("Task " + taskId + " is running in thread " + Thread.currentThread().getName());
            });
        }

        // Shutdown the pool after task submission
        fixedThreadPool.shutdown();
    }
}

2. Cached Thread Pool

A cached thread pool creates new threads as needed and reuses previously constructed threads (if available). This is suitable for executing many short-lived asynchronous tasks.

package org.kodejava.util.concurrent;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {
    public static void main(String[] args) {
        // Create a cached thread pool
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            cachedThreadPool.execute(() -> {
                System.out.println("Task " + taskId + " is running in thread " + Thread.currentThread().getName());
            });
        }

        // Shutdown the pool after task submission
        cachedThreadPool.shutdown();
    }
}

3. Single Thread Executor

A single-threaded executor ensures that tasks are executed sequentially, one at a time, in a single thread.

package org.kodejava.util.concurrent;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorExample {
    public static void main(String[] args) {
        // Create a single-threaded executor
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            singleThreadExecutor.execute(() -> {
                System.out.println("Task " + taskId + " is running in thread " + Thread.currentThread().getName());
            });
        }

        // Shutdown the pool after task submission
        singleThreadExecutor.shutdown();
    }
}

4. Scheduled Thread Pool

A scheduled thread pool is used to schedule tasks to run after a delay or periodically.

package org.kodejava.util.concurrent;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) {
        // Create a scheduled thread pool with 2 threads
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);

        // Schedule a task to run after a 3-second delay
        scheduledThreadPool.schedule(() -> {
            System.out.println("Task is running after a delay in thread " + Thread.currentThread().getName());
        }, 3, TimeUnit.SECONDS);

        // Schedule a repeating task to run every 2 seconds
        scheduledThreadPool.scheduleAtFixedRate(() -> {
            System.out.println("Repeating task is running in thread " + Thread.currentThread().getName());
        }, 1, 2, TimeUnit.SECONDS);

        // Optionally, shutdown the pool after some time (e.g., 10 seconds)
        scheduledThreadPool.schedule(() -> scheduledThreadPool.shutdown(), 10, TimeUnit.SECONDS);
    }
}

5. Custom Thread Pool

For more advanced needs, you can use ThreadPoolExecutor directly to fine-tune the behavior of the thread pool.

package org.kodejava.util.concurrent;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        // Create a custom thread pool with 2 core threads, 4 maximum threads, and a 10-task queue
        ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
                2, 4, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));

        for (int i = 1; i <= 10; i++) {
            final int taskId = i;
            customThreadPool.execute(() -> {
                System.out.println("Task " + taskId + " is running in thread " + Thread.currentThread().getName());
            });
        }

        // Shutdown the pool after task submission
        customThreadPool.shutdown();
    }
}

Key Points:

  • shutdown(): Prevents new tasks from being submitted to the thread pool and initiates an orderly shutdown.
  • shutdownNow(): Attempts to stop all actively executing tasks and halts the processing of waiting tasks.
  • newFixedThreadPool(): Creates a pool of a fixed number of threads.
  • newCachedThreadPool(): Creates a pool with potentially unlimited threads.
  • newSingleThreadExecutor(): Creates a single-threaded pool.
  • newScheduledThreadPool(): Creates a pool for scheduling tasks.

By using thread pools, you can effectively manage system resources and control the level of concurrency in your applications.

How do I use ExecutorService to run tasks in Java?

In Java, the ExecutorService interface is part of the java.util.concurrent package and provides a higher-level replacement for managing threads and tasks. It simplifies the execution of tasks in a multithreaded environment by abstracting thread creation and management.

Here’s how you can use ExecutorService to run tasks in Java:


1. Creating an ExecutorService

You can create an instance of ExecutorService using the factory methods provided by the Executors class. Some common options are:

  • Single-threaded pool:
    ExecutorService executor = Executors.newSingleThreadExecutor();
    
  • Fixed-size thread pool:
    ExecutorService executor = Executors.newFixedThreadPool(4); // 4 threads in the pool
    
  • Cached thread pool (dynamic sizing):
    ExecutorService executor = Executors.newCachedThreadPool();
    
  • Scheduled thread pool (for tasks that need scheduling or delayed execution):
    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
    

2. Submitting Tasks

You can submit tasks (runnable or callable) to the ExecutorService for execution:

  • Using Runnable:
    The Runnable interface doesn’t return a result or throw checked exceptions.

    executor.submit(() -> {
      System.out.println("Running a task in thread: " + Thread.currentThread().getName());
    });
    
  • Using Callable:
    The Callable interface allows the task to return a result and throw exceptions.

    Future<Integer> future = executor.submit(() -> {
      System.out.println("Calculating result in " + Thread.currentThread().getName());
      return 42; // returning a result
    });
    
    // Retrieve the result
    try {
      Integer result = future.get();
      System.out.println("Result: " + result);
    } catch (Exception e) {
      e.printStackTrace();
    }
    

3. Shutting Down the ExecutorService

You need to shut down the ExecutorService once you’ve completed submitting tasks:

  • Graceful shutdown:
    This stops accepting new tasks and allows the currently running tasks to complete.

    executor.shutdown();
    try {
      if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
          executor.shutdownNow(); // Force shutdown if timeout happens
      }
    } catch (InterruptedException e) {
      executor.shutdownNow();
    }
    
  • Forceful shutdown:
    This halts all running tasks and stops new ones immediately.

    executor.shutdownNow();
    

4. Example: Submitting Multiple Tasks

package org.kodejava.util.concurrent;

import java.util.concurrent.*;

public class ExecutorServiceExample {
    public static void main(String[] args) {
        // Create a fixed thread pool with 3 threads
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // Submit Runnable tasks
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // Simulate work
                } catch (InterruptedException e) {
                    System.err.println("Task " + taskId + " was interrupted!");
                }
            });
        }

        // Shutdown the executor gracefully
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow(); // Force shutdown if tasks exceed timeout
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }

        System.out.println("All tasks finished.");
    }
}

5. Choosing Between Runnable and Callable

  • Use Runnable when your task does not need to return a result.
  • Use Callable when your task needs to return a result or throw checked exceptions.

Advanced Features

If you need to manage periodic tasks or delayed execution, use ScheduledExecutorService:

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

// Schedule a task to run after a delay
scheduler.schedule(() -> System.out.println("Task executed after delay"), 3, TimeUnit.SECONDS);

// Schedule a task to run repeatedly at fixed intervals
scheduler.scheduleAtFixedRate(() -> System.out.println("Recurring task"), 1, 5, TimeUnit.SECONDS);

Summary

  1. Create an ExecutorService instance (e.g., fixed thread pool, cached thread pool).
  2. Submit tasks (Runnable or Callable) using submit().
  3. Shut down the executor service gracefully (shutdown() and awaitTermination()).
  4. Use Callable and Future for tasks that need to return results.

This abstraction helps manage your threads efficiently and avoids the complexities of low-level thread creation and management.

How do I manage sessions using HttpSession in Jakarta Servlets?

Managing sessions using HttpSession in Jakarta Servlets is a straightforward process. HttpSession is a part of the Jakarta Servlet API which provides a way to handle session management between a client and server during multiple requests.

Here’s a structured guide on using and managing sessions with HttpSession:


1. Introduction to HttpSession

  • The HttpSession interface is used to:
    • Store information about a user’s session (attributes such as user information, preferences, or data specific to each client).
    • Track users across multiple requests (via cookies or URL rewriting).

2. How to Create or Retrieve a Session

You can retrieve or create a session using the HttpServletRequest.getSession() method:

HttpSession session = request.getSession();
  • If a session already exists, this method returns the existing session.
  • If no session exists, it will create a new one.

If you want to retrieve the session but don’t want to create a new one if it does not exist, you can use:

HttpSession session = request.getSession(false); // Returns null if no session exists

3. Adding Attributes to the Session

You can store user-related or application-specific data in the session using the setAttribute method:

session.setAttribute("user", "JohnDoe");
session.setAttribute("cartItems", cartItemList);

4. Retrieving Attributes from the Session

Use the getAttribute method to retrieve values stored in the session:

String user = (String) session.getAttribute("user");
List<String> cartItems = (List<String>) session.getAttribute("cartItems");

Make sure to cast the returned object to the proper type.


5. Removing Attributes from the Session

Use the removeAttribute method to delete specific session attributes:

session.removeAttribute("user");

6. Invalidating or Destroying the Session

When the session is no longer needed (e.g., a user logs out), you can invalidate the session using:

session.invalidate();

This method:

  1. Invalidates the current session and removes all the stored attributes.
  2. Creates a new session on the next request.getSession() call.

7. Setting Session Timeout

You can specify the session timeout (in minutes) using:

session.setMaxInactiveInterval(30 * 60); // 30 minutes

To retrieve the current session timeout:

int timeout = session.getMaxInactiveInterval();

If the user is inactive for longer than the timeout duration, the session will be invalidated automatically.


8. Session ID

Each HttpSession has a unique session ID. You can retrieve it using:

String sessionId = session.getId();

This ID is used to track the session between requests (usually via cookies or URL rewriting).


9. Checking Session Validity

You can verify whether a session is new using:

boolean isNew = session.isNew();

This is particularly useful when you want to check if the session was newly created or reused.


10. Session Tracking Mechanisms

The server manages session tracking using one of the following mechanisms:

  1. Cookies: This is the default method where the session ID is maintained using a cookie (e.g., JSESSIONID).
  2. URL Rewriting: This is a fallback mechanism when cookies are disabled. The session ID is appended to the URL as a query parameter.

For enabling URL rewriting, you can use:

String encodedURL = response.encodeURL("yourUrlHere");

This ensures the session ID is included in the URL if cookies are not supported.


11. Example Code: Using HttpSession

Here’s a full working example:

package org.kodejava.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;

@WebServlet("/sessionExample")
public class SessionExampleServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
          throws ServletException, IOException {
    // Retrieve session, create if none exists
    HttpSession session = request.getSession();

    // Set session attributes
    session.setAttribute("username", "JohnDoe");

    // Get session attributes
    String username = (String) session.getAttribute("username");

    // Display session info
    response.setContentType("text/html");
    response.getWriter().println("<h1>Welcome, " + username + "</h1>");

    // Show session ID
    response.getWriter().println("<p>Session ID: " + session.getId() + "</p>");
  }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
          throws ServletException, IOException {
    // Invalidate session on logout
    HttpSession session = request.getSession(false);
    if (session != null) {
      session.invalidate();
    }
    response.getWriter().write("Logged out successfully!");
  }
}

12. Best Practices for HttpSession

  • Minimize Data Storage: Store only necessary, lightweight data in the session to reduce memory overhead.
  • Secure Session Handling:
    • Ensure cookies are marked as HttpOnly and Secure.
    • Implement proper session timeout.
    • Use HTTPS to protect the session ID during transmission.
  • Invalidate Sessions on Logout: Always invalidate the session to clear sensitive data when users log out.

By following these steps, you can effectively manage user sessions using HttpSession in your Jakarta Servlets-based application.

Maven dependencies

<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.1.0</version>
    <scope>provided</scope>
</dependency>

Maven Central