Java — Concurrency and Threads
Scenario: E-Commerce Order Processing System
Your system handles:
- Order placement (CPU-bound task)
- Payment processing (I/O-bound task)
- Inventory update (shared data)
- Notification sending (parallel + async)
- 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
LockorConcurrentMapfor 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
Lockthere.
### 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.
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
@Transactionalor 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
🔐 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:
- Thread A reads product with
version = 5. - Thread B reads same product (version = 5).
- Thread A updates stock → succeeds → version becomes 6.
- 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:
- Thread A selects with
FOR UPDATE→ locks the row. - 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.








