Saturday, June 22, 2024

πŸ”₯ Mastering Java Concurrency: Dive into Part 1! πŸš€

Programming LanguageπŸ”₯ Mastering Java Concurrency: Dive into Part 1! πŸš€


πŸ‘‹ 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

  1. πŸ”„ Understanding Concurrency
  2. 🧡What are Threads
  3. πŸ’ΎProblem with Thread Caching
  4. πŸ”’ Solution: Volatile Keyword
  5. 🏎️ Problem: Race Conditions
  6. πŸ› οΈ Solution: Synchronized Keyword
  7. 🧱 Synchronize Code Blocks
  8. πŸ” 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:

  1. Change the isStopped value to false.
  2. 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");
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

πŸ”’ 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;
    }
}

Enter fullscreen mode

Exit fullscreen mode

can you guess the output?

expect to be 2000 but value is: 1746
Enter fullscreen mode

Exit fullscreen mode

But why does this happen? Let’s understand it:

Race Condition

Look at the increment method:

  1. First, we store the value of the counter to local variable i.
  2. Then, we increment i by 1.
  3. Finally, we write i back to counter.

And two threads are doing this simultaneously.

Race Condition

πŸ› οΈ 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;
    }
}
Enter fullscreen mode

Exit fullscreen mode

🧱 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);
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

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

Image description

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! 🌟

Check out our other content

Check out other tags:

Most Popular Articles