Concurrency in Java
Modern computers have many cores, and servers handle many users at once. Java has extensive tools for running work concurrently, along with rules for doing it safely.
Threads
A thread is an independent path of execution. Java programs start with a main thread and can create more: Thread t = new Thread(() -> doWork()); t.start();. start() runs the code in a new thread; calling run() directly would run it in the current thread. join() waits for a thread to finish.
Why concurrency is hard
Threads share memory, so they can interfere. If two threads run count++ at once, both may read the same old value, and one increment is lost: a race condition. Such bugs depend on timing, so they may appear rarely and be hard to reproduce.
synchronized and locks
Marking a method or block synchronized lets only one thread at a time hold the object's lock and run it, preventing races. The java.util.concurrent.locks package offers more flexible locks such as ReentrantLock. Too much locking slows programs and can cause deadlock, so locks should be held briefly and acquired in a consistent order.
Atomic variables and thread-safe collections
AtomicInteger provides operations such as incrementAndGet that are safe without explicit locks. Thread-safe collections, such as ConcurrentHashMap and CopyOnWriteArrayList, let many threads share data. Ordinary ArrayList and HashMap are not thread-safe.
Executors and thread pools
Creating a thread per task is costly. An ExecutorService manages a pool of reusable threads: ExecutorService pool = Executors.newFixedThreadPool(4); pool.submit(task);. Submitting a Callable returns a Future, whose get() waits for and returns the result. Pools should be shut down when finished.
CompletableFuture and virtual threads
CompletableFuture lets you chain asynchronous steps: supplyAsync(() -> fetch()).thenApply(this::parse).thenAccept(this::show). Java 21 made virtual threads standard: very lightweight threads managed by the JVM, so a server can handle many thousands of simultaneous blocking tasks, each in its own simple thread.
Immutability and visibility
Objects that never change after construction, such as Strings and records, are automatically thread-safe. Without synchronisation, one thread's changes may not be visible to another; the volatile keyword guarantees visibility for a single variable. Designing with immutable data avoids many concurrency problems.
Worked example
Summing a large array with four threads: split it into four parts, submit four Callables that each return the sum of their part, then add the four Future results. For an array of 1 to 1,000,000, each part sums 250,000 numbers, and the total is 500,000,500,000. Using a long matters: the total exceeds int's maximum of about 2.1 billion.
Key takeaways
Threads run concurrently; start() launches one, and join() waits. Shared memory causes race conditions such as lost updates. synchronized blocks and locks give mutual exclusion but risk slowdowns and deadlock. AtomicInteger and ConcurrentHashMap are thread-safe tools. Executor thread pools reuse threads; Futures return results. CompletableFuture chains async steps; Java 21's virtual threads make huge numbers of threads cheap. Immutable objects are inherently thread-safe; volatile ensures visibility.
Flashcard
Independent path of execution
Check your understanding
Two threads each run count++ 1,000 times on a shared int, and the result is 1,874. What is the best fix?
