Sitemap

Java — Concurrency and Threads

5 min readJun 17, 2025

--

Scenario: E-Commerce Order Processing System

Your system handles:

  1. Order placement (CPU-bound task)
  2. Payment processing (I/O-bound task)
  3. Inventory update (shared data)
  4. Notification sending (parallel + async)
  5. Daily report generation (parallel recursive)

Goal: Use concurrency tools effectively to process orders efficiently and safely.

### 1. Thread / Runnable

✅ Use it when:

  • Task is simple, short-lived, and you don’t need return values.
  • Example: Logging the order.
Runnable logger = () -> System.out.println("Order logged: " + orderId);
new Thread(logger).start();

❌ Why not for other steps?

  • Poor control (no pool, no lifecycle mgmt).
  • Can lead to too many threads if overused.
  • No return values — unsuitable for tasks like payment.

### 2. ExecutorService

✅ Use it when:

  • You want controlled, reusable thread pooling.
  • Example: Handling multiple orders concurrently.
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> processOrder(order));

❌ Why not for complex workflows?

  • Not great for chaining tasks or non-blocking code.
  • You’d need to manually manage dependencies between steps (e.g., payment → email).

### 3. Callable + Future

✅ Use it when:

  • You need the result from a task.
  • Example: Processing payment — need to know if it succeeded.
Future<String> future = executor.submit(() -> processPayment(order));
String status = future.get(); // blocks until done

❌ Why not for:

  • Chaining or async flows like sending a follow-up email.
  • Future.get() blocks, so it's not good for high-performance, reactive flows.

### 4. synchronized / Lock

✅ Use it when:

  • You update shared state that must be thread-safe.
  • Example: Updating inventory count.
private final Lock lock = new ReentrantLock();
public void updateInventory(String itemId, int qty) {
lock.lock();
try {
inventory.put(itemId, inventory.get(itemId) - qty);
} finally {
lock.unlock();
}
}

❌ Why not for:

  • Independent tasks like email or payment (they don’t share data).
  • It slows down concurrency — only use where consistency is needed.

### 5. volatile

✅ Use it when:

  • You share a simple flag or signal between threads.
  • Example: Stopping the service gracefully.
volatile boolean running = true;
public void stopService() {
running = false;
}

❌ Why not for:

  • Ensuring atomicity or safe access to collections — use Lock or ConcurrentMap for that.
  • Doesn’t solve data consistency; only ensures visibility.

### 6. Concurrent Collections

✅ Use it when:

  • Threads read/write to a shared collection.
  • Example: Tracking active orders.
ConcurrentMap<String, Order> activeOrders = new ConcurrentHashMap<>();
activeOrders.put(order.getId(), order);

❌ Why not for:

  • Atomic transactions or complex logic inside methods — better with Lock there.

### 7. CompletableFuture

✅ Use it when:

  • You want non-blocking, async processing and chaining.
  • Example: Payment → Email → Audit → Update dashboard.
CompletableFuture.supplyAsync(() -> processPayment(order))
.thenApply(result -> updateDashboard(result))
.thenAccept(status -> sendConfirmationEmail(order));

❌ Why not for:

  • When steps must block or run in sequence with tight control.
  • Tricky error handling in deeply nested chains unless managed carefully.

### 8. Fork/Join Framework

✅ Use it when:

  • Task is CPU-bound, large, and splittable.
  • Example: End-of-day order summary (sum order values from 10,000+ orders).
ForkJoinPool pool = new ForkJoinPool();
int total = pool.invoke(new OrderSummaryTask(allOrders));

❌ Why not for:

  • I/O-bound tasks (e.g., API calls) — this is designed for CPU work.
  • Real-time operations — fork/join is more suited to batch jobs.
Press enter or click to view image in full size

Threads are expensive

Get Naveen’s stories in your inbox

Join Medium for free to get updates from this writer.

Creating a new thread:

  • Consumes memory (1MB+ per thread)
  • Adds CPU context switching overhead
  • Doesn’t scale well if you log thousands of events

Don’t try to use everything everywhere. Choose based on:

  • Task nature (CPU-bound vs I/O-bound)
  • Need for results (blocking vs async)
  • Data access (shared vs isolated)
  • Complexity (simple logging vs multistep workflow)

Concurrent operations involving a database require careful balancing of performance, thread management, and data integrity.

Goal: Perform concurrent operations (reads/writes) to a database safely and efficiently

1. Use a Thread Pool (ExecutorService)

  • Manages concurrent tasks like processing orders or background jobs.
  • Prevents too many simultaneous threads (which can overload DB).
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
Order order = fetchOrderFromDb(orderId);
processOrder(order);
});

✅ Why: Controlled concurrency
❌ Don’t: Spawn raw threads for each DB task

2. Use a Connection Pool (HikariCP, C3P0, etc.)

  • Manages DB connections efficiently for many threads.
spring.datasource.hikari.maximum-pool-size=20

✅ Always use a connection pool in production
❌ Never open a new
Connection per thread manually

3. Use Transactions Correctly

  • Ensures data consistency in concurrent updates.
  • Annotate with @Transactional or use manual transactions.
@Transactional
public void updateInventory(String productId, int qty) {
inventoryRepository.decrement(productId, qty);
}

✅ Ensures atomicity of operations
❌ Don’t allow concurrent writes without transaction safety

4. Avoid Overlapping Writes (Race Conditions)

  • Use:
  • Pessimistic Locking (e.g., SELECT FOR UPDATE)
  • Optimistic Locking (e.g., version column)
@Version
private int version; // for optimistic locking

✅ Prevents conflicting updates
❌ Without locking, two users could overwrite each other’s data

5. Use CompletableFuture for Parallel Read Operations

  • Parallelize independent DB reads (e.g., user data + product data).
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userRepo.findById(id));
CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(() -> productRepo.findById(id));
CompletableFuture.allOf(userFuture, productFuture).join();

✅ Improves throughput for non-conflicting reads
❌ Not for writes unless properly synchronized

6. Batch Writes When Possible

  • Use batch inserts/updates to reduce connection overhead.
jdbcTemplate.batchUpdate("INSERT INTO orders ...", batchArgs);

✅ Reduces DB round-trips
❌ Don’t write each record in a separate transaction

7. Use a Queue for Asynchronous Writes

  • If the operation isn’t critical immediately (e.g., analytics, logs), queue it:
  • Use in-memory queue (BlockingQueue)
  • Or message broker (Kafka, RabbitMQ)
logQueue.offer("Save analytics for order X");
executor.submit(() -> {
String log = logQueue.take();
analyticsRepo.save(log);
});

✅ Offloads DB writes, improves response time

❌ Don’t block user requests for non-critical inserts

Press enter or click to view image in full size

🔐 Locking Strategies:

✅ 1. Optimistic Locking

💡 Idea:

“I believe nothing will go wrong, but I’ll check before committing.”

  • No actual database locks are used during reading.
  • You read the data, make changes, and before saving, check if someone else changed it in the meantime.
  • If someone else did — you fail or retry.

🔧 How?

  • Use a version number or timestamp column.
@Entity
public class Product {
@Id
private Long id;
private int stock;
@Version
private int version; // auto-managed by JPA/Hibernate
}

🔄 Update Flow:

  1. Thread A reads product with version = 5.
  2. Thread B reads same product (version = 5).
  3. Thread A updates stock → succeeds → version becomes 6.
  4. Thread B tries to update → fails (version mismatch).

✅ Pros:

  • No locks → high performance, especially in read-heavy systems.
  • Works well when conflicts are rare.

❌ Cons:

  • Needs retry logic for failed updates.
  • Not ideal for frequently modified data (conflict rate becomes high).

✅ 2. Pessimistic Locking

💡 Idea:

“I don’t trust others. I’ll lock it so nobody else can touch it.”

  • The record is locked in the database when it’s read.
  • Other transactions must wait or fail if they try to access the same data.

🔧 How?

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findAndLockById(Long id);
SELECT * FROM product WHERE id = 123 FOR UPDATE;

🔄 Update Flow:

  1. Thread A selects with FOR UPDATE → locks the row.
  2. Thread B tries to select same row → must wait until A is done.

✅ Pros:

  • Prevents conflicts before they happen.
  • Safe for frequently updated records (like inventory).

❌ Cons:

  • Introduces blocking and possible deadlocks.
  • Slower in read-heavy scenarios.

--

--