StampedLock is a higher-performance alternative to ReadWriteLock with three modes:
StampedLock lock = new StampedLock();
// Optimistic read — no lock acquired, just get a stamp
long stamp = lock.tryOptimisticRead();
int x = this.x, y = this.y;
if (!lock.validate(stamp)) { // check if write happened during read
stamp = lock.readLock(); // fall back to real read lock
try { x = this.x; y = this.y; }
finally { lock.unlockRead(stamp); }
}
// Write lock
long writeStamp = lock.writeLock();
try { this.x = newX; this.y = newY; }
finally { lock.unlock(writeStamp); }
Optimistic reads are perfect for read-dominant workloads: no lock acquisition, just validate that no write occurred. Falls back to pessimistic if invalidated.
> StampedLock is NOT reentrant (unlike ReentrantReadWriteLock). Calling a stamped lock method from a thread that already holds it causes deadlock.
ScopedValue is the recommended replacement for ThreadLocal in the virtual thread era:
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
// Bind a value for the scope of a computation
ScopedValue.where(CURRENT_USER, authenticatedUser)
.run(() -> processRequest()); // value visible in entire call subtree
// Access anywhere in the call tree
User user = CURRENT_USER.get();
Why better than ThreadLocal for virtual threads:
- Immutable — value can't be changed during scope (thread-safe by design)
- Automatically cleaned up when scope ends (no
remove()needed) - Works correctly with structured concurrency (child tasks inherit parent's values)
- No risk of memory leaks in thread pools
Virtual threads are lightweight JVM-managed threads (not OS threads). You can create millions with minimal overhead.
// Create virtual thread
Thread.ofVirtual().start(() -> blockingIO());
// Virtual thread executor — one virtual thread per task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++)
executor.submit(this::handleRequest);
}
Internals:
- Mapped M:N onto platform (OS carrier) threads
- When a virtual thread blocks (I/O, sleep, lock), it unmounts from carrier thread; carrier serves other virtual threads
- Stack is heap-allocated, starts ~kilobytes vs ~1MB for OS threads
- No changes to Thread API — drop-in compatible
Pinning problem — virtual thread is pinned (can't unmount) when:
- Inside a
synchronizedblock that does I/O - Calling native (JNI) code
// BEFORE: synchronized (causes pinning in virtual thread era)
synchronized (this) { jdbcConnection.executeQuery(); } // pins!
// AFTER: use ReentrantLock instead
ReentrantLock lock = new ReentrantLock();
lock.lock();
try { jdbcConnection.executeQuery(); } // virtual thread can unmount
finally { lock.unlock(); }
Project Loom (finalized Java 21) fundamentally changes Java concurrency with virtual threads and structured concurrency.
Impact on existing code:
- Blocking JDBC, HTTP clients, file I/O become scalable
- Thread-per-request model (Tomcat, Spring MVC) now handles millions of concurrent requests
- No need to rewrite to reactive (WebFlux) for scalability
Framework support:
- Spring Boot 3.2+:
spring.threads.virtual.enabled=true - Tomcat/Jetty: virtual thread executor support
- JDBC: most drivers compatible (except if synchronized internally)
When to still use reactive:
- Built-in backpressure operators
- Fine-grained streaming pipelines
- Existing reactive codebase (refactoring cost not worth it)
The Java Memory Model gives final fields special guarantees: their values are guaranteed visible to all threads without synchronization after the object's constructor completes (as long as this doesn't escape the constructor).
class ImmutablePoint {
final int x;
final int y;
ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
// 'this' must not escape here (don't register this in constructor!)
}
}
// Safe publication — another thread reading x and y after the constructor
// sees the correct values without synchronization
Unsafe publication — sharing object reference before constructor completes:
// DON'T DO THIS
ImmutablePoint point;
ImmutablePoint(int x, int y) {
ExternalRegistry.register(this); // 'this' escapes before fields are set!
this.x = x;
this.y = y;
}
JFR (built into JVM since Java 11) is invaluable for diagnosing concurrency issues in production:
# Start recording
jcmd <pid> JFR.start duration=60s filename=app.jfr
# Or programmatically
Recording recording = new Recording();
recording.enable("jdk.ThreadStart");
recording.enable("jdk.MonitorEnter");
recording.enable("jdk.MonitorWait");
recording.enable("jdk.ThreadSleep");
recording.start();
Key JFR events for concurrency:
jdk.MonitorEnter— lock acquisition attempts (detect contention)jdk.MonitorWait— wait() calls with durationsjdk.ThreadBlocked— threads blocked on locksjdk.ThreadSleep— sleep eventsjdk.VirtualThreadPinned— virtual thread pinning events (Java 21+)
Analyze with JDK Mission Control (JMC): Flame graphs, lock contention, thread activity timelines.
The LMAX Disruptor is a high-performance inter-thread messaging library that achieves extremely low latency by using:
Traditional Queue: Disruptor:
Producer → Queue → Consumer Producer → RingBuffer → Consumer
(locks, GC) (lock-free, pre-allocated)
Key concepts:
- RingBuffer: Fixed-size circular array, pre-allocated; no GC pressure
- Sequence: Atomic counter tracking progress for each producer/consumer
- WaitStrategy: How consumers wait for events (BusySpin, Yielding, Blocking, Sleeping)
- EventProcessor: Threads consuming events from the ring buffer
Why faster than BlockingQueue:
- No locks (CAS only)
- Cache-friendly (array, not linked nodes)
- No garbage (pre-allocated event objects reused)
- False sharing avoided with padding
Used by: Log4j2's async loggers, financial trading systems, game engines.
For ultra-low-latency systems, thread-to-CPU core affinity eliminates OS scheduling overhead:
// Using Java Thread Affinity library (OpenHFT)
try (AffinityLock lock = AffinityLock.acquireCore()) {
// This thread is now pinned to a specific core
performLatencySensitiveWork();
}
NUMA (Non-Uniform Memory Access): In multi-socket servers, memory access cost depends on which socket the memory is on. Threads should access memory local to their CPU socket.
# JVM NUMA awareness -XX:+UseNUMA -XX:+UseParallelGC # GC that's NUMA-aware
ForkJoinPool NUMA:
- Java 14+:
ForkJoinPoolhas NUMA-aware allocation option - Critical for big-data processing on multi-socket machines
Java NIO's Selector enables one thread to manage many I/O channels:
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // blocks until at least one channel ready
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) { handleAccept(key); }
if (key.isReadable()) { handleRead(key); }
iter.remove();
}
}
NIO.2 Async I/O (Java 7+): Callback-based, no selector needed:
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
channel.read(buffer, 0, null, new CompletionHandler<Integer, Void>() {
public void completed(Integer bytesRead, Void attachment) { process(buffer); }
public void failed(Throwable exc, Void attachment) { handleError(exc); }
});
With virtual threads, blocking I/O is now equivalent in scalability to NIO for most use cases.
ConcurrentSkipListMap is a lock-free, sorted, thread-safe map:
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
map.put(3, "three");
map.put(1, "one");
map.put(2, "two");
// Sorted access operations — all thread-safe without locking
map.firstKey(); // 1
map.lastKey(); // 3
map.headMap(2); // {1=one}
map.tailMap(2); // {2=two, 3=three}
map.floorKey(2); // 2 (greatest key ≤ 2)
map.ceilingKey(2); // 2 (smallest key ≥ 2)
Skip list internal: Probabilistic data structure with multiple layers. Each level skips over more elements. Expected O(log n) for all operations, fully lock-free using CAS.
When to use over ConcurrentHashMap: When you need sorted key access + range queries + concurrent writes.
The Reactive Streams specification (Java 9 java.util.concurrent.Flow) defines a standard for asynchronous stream processing with non-blocking backpressure:
// Publisher produces items
Publisher<T> publisher = ...;
// Subscriber consumes items
publisher.subscribe(new Subscriber<T>() {
Subscription subscription;
public void onSubscribe(Subscription s) {
this.subscription = s;
s.request(10); // request 10 items (backpressure signal)
}
public void onNext(T item) {
process(item);
subscription.request(1); // request one more
}
public void onError(Throwable t) { handleError(t); }
public void onComplete() { cleanup(); }
});
Project Reactor (Spring WebFlux) and RxJava implement this spec. Key types:
Mono<T>: 0 or 1 item asynchronouslyFlux<T>: 0 to N items asynchronously
Proper executor shutdown is critical for production applications:
ExecutorService executor = Executors.newFixedThreadPool(4);
// Submit tasks...
// Graceful shutdown pattern
executor.shutdown(); // stop accepting new tasks
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow(); // cancel in-progress tasks
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
log.error("Executor did not terminate");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt(); // preserve interrupt status
}
Spring Boot graceful shutdown:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
Virtual thread executor with try-with-resources (Java 19+):
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// executor.close() called automatically — waits for all tasks to complete
IntStream.range(0, 1000).forEach(i -> executor.submit(() -> handleRequest(i)));
}
Proper thread interruption is important for clean cancellation:
// WRONG: Swallowing interrupt
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// DON'T just catch and ignore!
}
// RIGHT: Restore interrupt status or propagate
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore interrupt status
return; // or throw new RuntimeException(e) to propagate
}
// Check interrupt in long-running loops
while (!Thread.currentThread().isInterrupted()) {
doWork();
}
interrupt() vs stop(): stop() is deprecated (unsafe — leaves objects in inconsistent state). interrupt() is the cooperative cancellation mechanism — the thread must check and respond to the interrupt.
// handle() — runs on success OR failure, transforms result
CompletableFuture<String> result = fetchUser()
.handle((user, ex) -> {
if (ex != null) return "default-user"; // on error
return user.getName(); // on success
});
// whenComplete() — runs on success OR failure, doesn't transform
fetchUser().whenComplete((user, ex) -> {
if (ex != null) metrics.incrementErrorCount();
else metrics.incrementSuccessCount();
// returns same CompletableFuture with original value/exception
});
// exceptionally() — only on failure, provide fallback
fetchUser().exceptionally(ex -> User.anonymous());
// Timeout (Java 9+)
fetchUser()
.orTimeout(5, TimeUnit.SECONDS) // complete exceptionally on timeout
.completeOnTimeout(User.anonymous(), 5, TimeUnit.SECONDS); // provide default
When the thread pool queue is full and all threads are busy, the RejectedExecutionHandler decides what to do:
| Policy | Behavior | Use Case |
|---|---|---|
AbortPolicy (default) | Throws RejectedExecutionException | Fail fast; caller handles |
CallerRunsPolicy | Caller thread executes the task | Natural backpressure; slows producer |
DiscardPolicy | Silently discards the task | Fire-and-forget, acceptable loss |
DiscardOldestPolicy | Removes oldest queued task, retries | Fresh tasks are priority |
// Custom rejection policy — log + fallback
new ThreadPoolExecutor(4, 8, 60L, SECONDS,
new ArrayBlockingQueue<>(200),
(task, executor) -> {
metrics.incrementRejectedTasks();
log.warn("Task rejected, falling back to sync execution");
if (!executor.isShutdown()) task.run(); // run in caller's thread
}
);
// GOOD: Use fork() for async subtask, compute() for current thread's work
protected Long compute() {
if (size <= THRESHOLD) {
return computeSequentially();
}
SumTask left = new SumTask(lo, mid);
left.fork(); // dispatch left to pool asynchronously
Long rightResult = new SumTask(mid, hi).compute(); // use current thread
Long leftResult = left.join(); // wait for left
return leftResult + rightResult;
}
// BAD: Forking both and joining both
left.fork(); right.fork();
return left.join() + right.join(); // current thread sits idle during both joins!
Tuning ForkJoinPool:
ForkJoinPool pool = new ForkJoinPool(
Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, // uncaught exception handler
true // asyncMode (FIFO vs LIFO for local queue)
);
| Use Case | Best Choice | Why |
|---|---|---|
| Read-heavy map (no order needed) | ConcurrentHashMap | Lock-free reads |
| Sorted concurrent map | ConcurrentSkipListMap | Lock-free, sorted |
| High-contention counter | LongAdder | Striped cells reduce contention |
| Single-producer/consumer queue | ArrayBlockingQueue | Simple, bounded |
| Multi-producer/consumer queue | LinkedTransferQueue | Better throughput |
| Read-dominant list | CopyOnWriteArrayList | Lock-free reads |
| Priority-based work queue | PriorityBlockingQueue | Heap-based, thread-safe |
Common sources:
- ThreadLocal without remove():
// In thread pools, ThreadLocal values survive thread reuse static final ThreadLocal<HeavyObject> tl = new ThreadLocal<>(); // After request: tl.remove() MUST be called!
- Static ConcurrentHashMap growing unbounded:
static final Map<String, byte[]> cache = new ConcurrentHashMap<>(); // No eviction policy → heap exhaustion over time // Fix: Use Caffeine/Guava Cache with size/time eviction
- Listener/Observer not unregistered:
eventBus.subscribe(this); // strong reference from eventBus to this // If 'this' is expected to be garbage collected, it won't be // Fix: Use WeakReference listeners or explicit unsubscribe
- Async tasks holding references:
CompletableFuture.supplyAsync(() -> process(largeObject)); // largeObject captured // If the future is stored, largeObject stays alive until future completes
The Active Object pattern decouples method execution from invocation. Each active object has its own thread and a queue for method requests:
class ActiveLogger {
private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
private final Thread worker;
ActiveLogger() {
worker = Thread.ofVirtual().start(() -> {
while (!Thread.interrupted()) {
try {
queue.take().run(); // process log requests sequentially
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
public void log(String message) {
queue.offer(() -> writeToFile(message)); // non-blocking submit
}
}
Used in: Async logging (Log4j2's async appender), actor frameworks (Akka), UI event loops.
Safe publication means making an object reference available to other threads in a way that guarantees they see the object's fully initialized state.
Safe publication mechanisms:
- Initialize in static initializer (class loading guarantees visibility)
- Store in
volatilefield - Store in
finalfield (JMM final field guarantee) - Store via lock: synchronized block or concurrent collection
// UNSAFE publication
class Holder {
private int n;
Holder(int n) { this.n = n; }
void assertSanity() {
if (n != n) throw new AssertionError(); // can actually happen!
}
}
public Holder holder; // non-volatile, non-final — unsafe!
// SAFE publication
public volatile Holder holder; // option 1
public final Holder holder = ...; // option 2 (if in constructor)
// store in ConcurrentHashMap // option 3
// store in synchronized block // option 4
Records are inherently thread-safe due to immutability:
// Record components are final — naturally thread-safe
record Transaction(String id, BigDecimal amount, Instant timestamp) {}
// Safe to share across threads without synchronization
Transaction tx = new Transaction(uuid, amount, Instant.now());
// Multiple threads can read tx concurrently — no sync needed
Records in concurrent maps:
// Records make excellent ConcurrentHashMap keys (final hashCode/equals)
ConcurrentHashMap<TransactionKey, TransactionResult> results = new ConcurrentHashMap<>();
record TransactionKey(String accountId, String transactionId) {}
// hashCode and equals generated from final components — always consistent
With virtual threads, ThreadLocal use requires extra care:
// PROBLEM: ThreadLocal in virtual threads
// Each virtual thread gets its own ThreadLocal copy — potentially millions of copies!
static final ThreadLocal<HeavyConnectionPool> pool = new ThreadLocal<>();
// With 1M virtual threads → 1M ConnectionPool instances → OOM
// SOLUTION 1: Use ScopedValue instead (Java 21+)
static final ScopedValue<Connection> CONNECTION = ScopedValue.newInstance();
ScopedValue.where(CONNECTION, getConnection()).run(() -> doWork());
// SOLUTION 2: Pass dependencies explicitly (dependency injection)
void process(Connection conn, Data data) { ... } // explicit is clear
// SOLUTION 3: If ThreadLocal is needed, keep values lightweight
static final ThreadLocal<SimpleDateFormat> formatter = // OK — lightweight
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
In distributed systems, local Java locks don't work across JVM instances. Need distributed coordination:
// Redis-based distributed lock (Redisson)
RLock lock = redissonClient.getLock("order-processing-lock-" + orderId);
try {
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); // wait 10s, hold 30s
if (!locked) throw new LockNotAcquiredException("Another instance processing order");
processOrder(orderId);
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
// Database-based lock (Optimistic Locking with JPA @Version)
@Entity
public class Order {
@Version
private Long version; // automatically incremented, conflicts detected
}
Distributed lock challenges:
- Clock drift between nodes can cause lock expiry issues
- Network partitions can cause split-brain (two nodes think they hold the lock)
- Redlock algorithm (multiple Redis masters) reduces risk but has controversies
1. Double-Checked Locking without volatile (pre-Java 5 bug, still seen):
// BROKEN (without volatile):
private static Singleton instance;
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton(); // object partially visible without volatile!
}
}
// FIX: Add volatile to instance field
2. Synchronized on non-final field:
// BROKEN: lock object can change
private Object lock = new Object();
synchronized(lock) { ... }
// If another thread assigns: lock = new Object(); — different lock!
// FIX: Make lock field final
3. Calling wait() without holding the lock:
// BROKEN: IllegalMonitorStateException lock.wait(); // must be inside synchronized(lock)
4. Using HashMap in multi-threaded code:
// In Java 7: causes infinite loop (resize causes circular linked list) // In Java 8+: race conditions, wrong results // ALWAYS use ConcurrentHashMap for shared maps
5. sleep() as synchronization mechanism:
// BROKEN: Timing-based, fragile Thread.sleep(1000); // hope the other thread is done by now
Key metrics to monitor:
// Thread pool monitoring via JMX
ThreadPoolExecutor tpe = (ThreadPoolExecutor) executorService;
int active = tpe.getActiveCount(); // actively executing threads
long completed = tpe.getCompletedTaskCount();
int queueSize = tpe.getQueue().size(); // backlog
int poolSize = tpe.getPoolSize();
// Expose via Micrometer (Spring Boot Actuator)
new ExecutorServiceMetrics(executorService, "myPool", tags)
.bindTo(meterRegistry);
// Exposes: pool.size, active.threads, queued.tasks, completed.tasks
JVM thread metrics:
# Via jstat jstat -gcutil <pid> 1000 # GC stats every second # Via JFR analysis - Thread blocked time - Lock contention heat map - Thread CPU time breakdown
Alerts to set:
- Queue size > 80% of capacity → producer-consumer imbalance
- Active threads = pool size → all threads busy, consider scaling
- Rejected task rate > 0 → pool overloaded
- Average task duration trending up → downstream degradation
Exceptions in background threads that aren't handled silently die:
// PROBLEM: Exception in thread dies silently
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> {
throw new RuntimeException("This silently disappears!");
});
// SOLUTION 1: Always get() the Future and handle exceptions
Future<?> future = executor.submit(() -> riskyWork());
try {
future.get(); // throws ExecutionException wrapping original
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // original exception
handleError(cause);
}
// SOLUTION 2: Set UncaughtExceptionHandler
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, ex) -> {
log.error("Uncaught exception in thread {}", thread.getName(), ex);
metrics.incrementUnhandledErrors();
});
return t;
};
// SOLUTION 3: CompletableFuture with exceptionally
CompletableFuture.runAsync(() -> riskyWork())
.exceptionally(ex -> { handleError(ex); return null; });
| ConcurrentLinkedQueue | LinkedBlockingQueue | |
|---|---|---|
| Blocking | No (returns null/false) | Yes (take/put block) |
| Bounded | No (unbounded) | Optional (can be bounded) |
| Algorithm | Lock-free (CAS) | Two-lock (head lock + tail lock) |
| Best for | Non-blocking producers/consumers | Blocking producer-consumer pattern |
| Size | size() is O(n) (traverse) | size() is O(1) |
// Non-blocking usage with ConcurrentLinkedQueue ConcurrentLinkedQueue<Task> queue = new ConcurrentLinkedQueue<>(); Task task = queue.poll(); // returns null if empty (non-blocking) if (task != null) process(task); // Blocking usage with LinkedBlockingQueue LinkedBlockingQueue<Task> queue = new LinkedBlockingQueue<>(100); Task task = queue.take(); // blocks until element available
Modern JVMs heavily optimize synchronized via several techniques:
1. Biased Locking (Java 1.6–14, removed in Java 21):
- First thread to acquire a lock "biases" the object toward itself
- Subsequent acquisitions by the same thread are nearly free (no CAS)
- Only pays cost when another thread contends
2. Lightweight Locking (thin lock):
- Low-contention case: object's mark word stores a CAS-based lock
- No OS mutex, no thread blocking — just spinning
3. Heavyweight Locking (inflated lock):
- High-contention: inflates to OS mutex with thread blocking
- Standard monitor semantics
4. Lock Elision:
- JIT detects that a lock is not accessible to other threads
- Removes the lock entirely (e.g., synchronized on a local variable)
5. Lock Coarsening:
- JIT merges adjacent synchronized blocks on the same monitor into one larger block
- Reduces lock/unlock overhead in tight loops
@Service
public class DashboardService {
@Async("asyncExecutor") // runs on asyncExecutor thread pool
public CompletableFuture<UserProfile> getUserProfileAsync(Long userId) {
return CompletableFuture.completedFuture(userService.getProfile(userId));
}
@Async("asyncExecutor")
public CompletableFuture<List<Order>> getOrdersAsync(Long userId) {
return CompletableFuture.completedFuture(orderService.getOrders(userId));
}
// Combine parallel calls
public Dashboard getDashboard(Long userId) throws Exception {
CompletableFuture<UserProfile> profileFuture = getUserProfileAsync(userId);
CompletableFuture<List<Order>> ordersFuture = getOrdersAsync(userId);
// Wait for both to complete
CompletableFuture.allOf(profileFuture, ordersFuture).join();
return new Dashboard(profileFuture.get(), ordersFuture.get());
}
}
// Configure async executor in Spring
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
Q: How does ConcurrentHashMap achieve lock-free reads?
A: Each Node in the array has a volatile value field. Reads traverse nodes using volatile reads which have happens-before guarantees with the last volatile write. No lock needed because: (1) array reference is volatile, (2) node values are volatile, (3) structural modifications use CAS + synchronized on bin head which establish happens-before with readers.
Q: Why is volatile long needed for 64-bit values on 32-bit JVMs?
A: 64-bit long and double reads/writes are not guaranteed to be atomic on 32-bit JVMs (JMM allows tearing — two separate 32-bit operations). volatile upgrades these to atomic 64-bit operations.
Q: Can you deadlock with ReentrantLock and no nested locking?
A: Yes. If a thread acquires a ReentrantLock and the thread is interrupted/killed before calling unlock(), the lock is never released. Always use lock in try block with unlock() in finally.
Q: What is a memory barrier and when does Java insert one?
A: A memory barrier (fence) is a CPU instruction that prevents instruction reordering across it and forces cache flush. Java inserts barriers: before/after volatile read/write, on lock acquire/release, on Thread.start()/Thread.join(). This implements happens-before.
Q: Explain the difference between submit() and execute() in ExecutorService.
A: execute(Runnable) is fire-and-forget — no way to know when it completes or if it threw an exception. submit() returns a Future — you can call get() to block and retrieve results, and any exception thrown is wrapped in ExecutionException which you can access via future.get().