π Hello there! Welcome to our Java Concurrency Series! π
π In this series, we'll dive into various aspects of Java concurrency, from basic to advanced topics. We'll begin by tackling fundamental concepts such as thread caching issues, basic thread synchronization, and utilizing multiple locks using synchronized code blocks. Stay tuned for an insightful journey into the world of multithreading!
Table of Contents
- π Understanding Concurrency
- π§΅What are Threads
- πΎProblem with Thread Caching
- π Solution: Volatile Keyword
- ποΈ Problem: Race Conditions
- π οΈ Solution: Synchronized Keyword
- 𧱠Synchronize Code Blocks
- π Locking
π Understanding Concurrency
Concurrency is where your computer can handle multiple tasks simultaneously. It involves managing the execution of multiple tasks concurrently to improve performance and responsiveness in software applications.
𧡠What are Threads
Thread is like a separate path of execution within a program. When a program is running, it can have multiple threads running concurrently, each performing its own set of instructions independently.
πΎ Problem with Thread Caching
In the code below, we create the Clock
class as a Runnable
and run it by spawning a new Thread
from the main class. Once started, the Clock
class's run method will keep running the while loop. To stop this, we need to call the cancel
method from the main thread, which will:
- Change the
isStopped
value to false. - Write it in the memory.
However, the thread running the while loop may not have the updated value of isStopped
and could still have a local cache of the old value of isStopped
(false).
public class Main {
public static void main(String[] args) throws InterruptedException {
Clock clock = new Clock();
Thread thread = new Thread(clock);
thread.start();
Thread.sleep(10000);
clock.cancel();
}
}
class Clock implements Runnable {
private volatile boolean isStopped = false;
public void cancel() {
this.isStopped = true;
}
public void run() {
while (!this.isStopped) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Tick");
}
}
}
π Solution: Volatile Keyword
The values of the volatile
variable will never be cached, and all writes and reads will be done to and from the main memory. So, the above thread
running the while
loop will always have the latest read value from the memory.
ποΈ Problem: Race Conditions
Let's consider the scenario where we create the Counter
class and try to update the counter
value from two separate threads.
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(counter);
Thread t2 = new Thread(counter);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("expect to be 2000 but value is : " + counter.getCounter());
}
}
public class Counter implements Runnable{
private int counter=0;
@Override
public void run() {
for(int i=0;i<1000;i++){
increament();
}
}
public void increament(){
int i =counter;
counter = i+1;
}
public int getCounter(){
return this.counter;
}
}
can you guess the output?
expect to be 2000 but value is: 1746
But why does this happen? Let's understand it:
Look at the increment
method:
- First, we store the value of the counter to local variable
i
. - Then, we increment
i
by 1. - Finally, we write
i
back tocounter
.
And two threads are doing this simultaneously.
π οΈ Solution: Synchronized Keyword
We can make the method synchronized so that only one thread can run the method at a time. Other threads have to wait for their turn to execute the method. This leads to consistent data.
We define a method to be synchronized using the synchronized
keyword.
public class Counter implements Runnable{
private int counter=0;
@Override
public void run() {
for(int i=0;i<1000;i++){
increament();
}
}
public synchronized void increament(){
int i =counter;
counter = i+1;
}
public int getCounter(){
return this.counter;
}
}
𧱠Synchronize Code Blocks
If two methods are dealing with two different variables, instead of making the entire method synchronized and locking the entire object, we can use synchronized code blocks to lock only the critical sections of each method. This approach allows us to ensure that no two threads can access the same method at the same time, providing better concurrency.
public class ExampleClass {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private int value1 = 0;
private int value2 = 0;
public void incrementValue1() {
synchronized (lock1) {
value1++;
System.out.println("Value 1 incremented to: " + value1);
}
}
public void decrementValue2() {
synchronized (lock2) {
value2--;
System.out.println("Value 2 decremented to: " + value2);
}
}
}
For instance, imagine we have incrementValue1()
and decrementValue2()
methods, each modifying separate variables value1
and value2
. We have declared two private final objects lock1 and lock2. These objects are used as locks for synchronized blocks to ensure thread safety when accessing the corresponding variables. By using synchronized code blocks within each method, we can lock only the sections where the shared variables are modified, allowing concurrent access to other methods while maintaining thread safety.
π Locking
Locks in concurrency are used to achieve thread-safety by enabling synchronous access to shared resources.
- Intrinsic Locks
In Java, every object has a built-in lock called the intrinsic lock. When a thread wants to access a shared object, it must first acquire this lock. This ensures that only one thread can access the object at a time. When you declare that a method as synchronized
, the thread calling that method acquires the intrinsic lock for that methodβs object and releases it when the method exits
For example, consider a Counter
class where each thread increments a shared counter variable. In this class, the increment()
method is declared as synchronized, which means only one thread can execute it at a time for a given Counter
object. When a thread calls the increment()
method, it automatically acquires the intrinsic lock associated with that method's object, ensuring that no other thread can modify the counter concurrently. Once the method completes its execution, the lock is released, allowing other threads to call the method.
π Thank you for joining us on this Java Concurrency Series journey! π We'll delve into CountdownLatch, Callable, Futures, and Semaphores soon.
π’ We'd love your feedback to improve our content. Share your thoughts with us!
π€ Stay connected, happy coding! π
Top comments (7)
everything in a program at some point can be viewed as a series of values in memory(random or persistent). I think this excellent work here could be even more valuable if you encapsulated it in the language of the JMM(java memory model)
Nice one, saving for the read later on
Nice post
Thanks Aries
Nice post,Keep it up!
Part 2: dev.to/mjsf1234/mastering-java-con...
Looking forward to the rest in the series.