Java: Multithreading — Part 1
Process vs Threads
Process = Multiple applications running simultaneously in the server, PC or Mac
Thread =Multiple tasks running within a process
Process — When a software application starts running, it uses system resources such as I/O devices, CPU, RAM, HDD, and probably network resources as well — courtesy OS. Similarly, other software applications will also want to use the same system resources simultaneously. In order for multiple software applications to share the system resources, they have to have clear boundaries between them. Otherwise, they will step on each other’s toes. OS enables this isolation by way of “processes”. Since multiple processes can run at the same time, an OS multitasks via Processes!
Thread — A software application, say a word document, may have to multitask between handling user events and saving the current work. To achieve this, each of those tasks needs to access the same system resources that are available to the process but within its own space. Similar to OS, each process is capable of providing this isolation to multiple tasks running within it by way of “threads”. Since multiple threads can run at the same time within the process, a process multitasks via Threads!
Single vs Multithreaded application
Single Threaded application
Analogy: A restaurant with 8 member kitchen department serving just one table at any given time because they have only one waiter. If there are more guests, they are asked to wait in the lobby.
Cons:
* Frustrated guests who are waiting in the lobby
* Wasted resources in the kitchen
Pros:
* Reduced complexity — one order at a time!
Multi-threaded application
Analogy: A restaurant with 8 member kitchen department serving 16 tables at any given time because they have 4 waiters.
Pros:
* Happy guests as waiting time is greatly reduced
* Efficient resource utilization
Cons:
* Application complexity due to shared resources
As the restaurant now has introduced more waiters (Threads), it is now utilizing the kitchen (CPU) staff (Cores) efficiently. As a result, it is serving more guests (Users) at any given time
The same applies to software applications. Essentially resulting in significant improvement in responsiveness because of concurrent execution.
Similarly, long-running tasks that can be split into multiple independent sub-tasks can be run in parallel in multiple threads, resulting in significant improvement in performance of the application.
Responsiveness due to concurrency and Performance due to parallelism are the motivations for multi-threaded applications
Heap vs Stack
The code that we write is executed by a thread(s). When a Java application starts up, main thread (main method) is spawned by the Java process — this is the application entry point. From this point onwards, entire application logic is executed either in the main thread or the threads that we spawn from within the application to either achieve concurrency or parallelism as explained above.
Application logic executed by threads involves computation being performed in the CPU, while the result of the computation is stored in RAM.
Each thread will wait for its turn to use one CPU core to perform computation and a local memory area (Stack and multiple frames within the stack for each method call) to store the results of computation temporarily. Once the thread completes code execution, it typically flushes the result back to RAM (Heap).
Heap is the shared memory area among threads where all the objects live. Stack is the private memory area allocated to each of the running thread.
Heap memory is garbage collected to free up precious space by removing objects that are no longer used (referenced) within the application while the memory space held by the stack is freed up once the thread of execution completes
Heap
All the objects created within a Java application is allocated space in memory called Heap. These objects live as long as it is referenced from somewhere in the application.
Stack
Threads executing a method or invoking a series of methods would need space in memory for storing local variables and method arguments — this memory area is called Stack. Each method called by the thread is stacked on top of the prior method-call called “stack frames”.
Locks
Monitor & Lock
When an object and its state is shared across multiple threads, any modification done to the state (eg. Page hit counter) must be done as a single operation (atomic). Otherwise, the object’s state would be corrupted by concurrent modification. Atomicity is accomplished by guarding the critical block of code with a lock to enforce mutual exclusion between competing threads.
Every object has an intrinsic lock called a monitor. Because of this language provision, locking the critical block of code is easily accomplished by adding the keyword — synchronized to the method signature. In the below program, the open() method of the safe object can be accessed by a thread, only after acquiring the intrinsic lock — note the synchronized keyword in the method signature.
A thread executing a synchronized method is said to have acquired the object’s lock. Till the moment this thread completes execution of the method, no other thread can execute this or any other synchronized method of this object.
But the thread that already has access to the object’s lock can call other synchronized methods without reacquiring the lock. This mechanism is called reentrant locking or reentrant synchronization.
Another important aspect of locks or synchronization is to establish a happens-before relationship. Meaning, any modification that is done to the state of an object within a synchronized block is guaranteed to be visible to other threads that will subsequently be accessing the same state by acquiring the same lock.
Two ways to acquire an object’s lock are
-By adding the synchronized keyword on the method as shown above
-By using the synchronized statement on ‘this’, Class object or any object
Using the synchronized statement is the preferred approach, as it will not block all the instance methods but only the block it wants to guard against concurrent access leading to improved performance.
Instance methods are synchronized on ‘this’
Static methods are synchronized on the ‘Class’ object
In essence, the effect of synchronization (or locking) is to prevent the following error conditions in a multi-threaded application
- Thread interference — Multiple threads modifying the state of an instance concurrently and corrupting it in the process due to the difference in time that the competing threads are scheduled to run, causing them to execute the same set of operations out of order.
- Memory inconsistency error — Reader threads will see stale data even when the writer thread completes the execution before the read. This is because the writer thread would store the changed value its CPU cache rather than flushing it into the main memory where all other threads can see the updated state.
When competing threads modify/modify or modify/read shared mutable state, without proper synchronization, involving non-atomic operations, leads to “race condition” and “memory inconsistency errors” due to interleaving
Summary
- Hardware resources such as CPU, RAM, HDD, network devices are made available to software applications by the operating system for computation.
- These resources are shared among multiple applications running concurrently, as processes, in the operating system. An operating system’s power lies in its ability to multi-task via processes.
- Likewise, an individual software application running as a process must multi-task, in order to efficiently utilize hardware resources. A process’s power lies in its ability to multi-task via concurrently running threads.
- Responsiveness through concurrency (eg.: A servlet serving multiple users concurrently), Performance through parallelism (eg.: invoking multiple HTTP endpoints in parallel to serve a user request) are the motivations for multi-threaded applications.
- A thread runs in a CPU core. Any application code runs in a thread. When a Java application is started, JVM invokes the main() method from within the main thread.
- Heap is the common memory area allocated to all threads to store objects. Any thread that has a reference to the object in heap can read/modify the object.
- Stack is the private memory area allocated to each thread (eg.: two threads calling a common utility method simultaneously will execute the method within its own stack where local variables and method arguments of one thread is not visible to the other thread)
- Multiple threads accessing the same object in heap must do so synchronously after acquiring a common lock in order to prevent corrupting the state of the object.
Note:
If you find some points being repeated, it was intentional. Please let me know your suggestions in the comments section. Cheers!
Reference:
Java Concurrency in Practice, a book by Brian Goetz, Doug Lea, Tim Peierls, David Holmes
Courtesy:
- Code images created with https://carbon.now.sh
- Diagrams created with https://www.draw.io
📝 Read this story later in Journal.
🗞 Wake up every Sunday morning to the week’s most noteworthy Tech stories, opinions, and news waiting in your inbox: Get the noteworthy newsletter >