Contents

Java Concurrency The Complete Basics Part - 2

To Read Part 1 - Basic Concurrency Concepts with Java

Data and Resources Sharing

Working with data and resources

The very basics of datas in java are often grouped into two main memory regions. Heap and Stack. Java Memory Model is a whole different subject itself so let’s stick with Heap and Stack for simplicity.

Heap Stack
Data lives here and can be used by any parts of the application. Here Methods get pushed-in and popped-out. Short-lived and only used by one thread of execution (i.e main thread or other thread).
Objects, Class members, Static variables. Local variables( primitive types and references).

[See: Java Memory Model]

The resources can be any representation of datas or objects.
i.e it can be any variables, flags, Arrays, File, IO devices, connections etc …. .

In a multithreaded environment where multiple tasks are executing concurrently, working with the resources/data isn’t straight forward. As i mentioned earlier, Complex relation between multiple threads, not handled properly causes anomalies, Race conditions, Data race and Deadlock”.

Race conditions, Data race and Deadlock

Race conditions:
When two or more threads access shared data and try to change it at the same time (modify, write at the same time). In short and clear - both threads “racing” to access/change the data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.util.ArrayList;
import java.util.List;

public class A3_RaceConditions {
    public static void main(String[] args) throws InterruptedException {
        List<Thread> threads = new ArrayList<>();

        // Creating 10 "IncrementerTask" threads and starting them
        IncrementalData incrementalData = new IncrementalData();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new IncrementerTask(incrementalData));
            thread.start();
            threads.add(thread);
        }

        // Wait until all threads are finished
        for (Thread thread : threads) {
            thread.join();
        }

        // Lets Check the final result
        System.out.println("Result value: " + incrementalData.getValue());

    }

    public static class IncrementerTask implements Runnable {

        private IncrementalData incrementalData;

        public IncrementerTask(IncrementalData incrementalData) {
            this.incrementalData = incrementalData;
        }

        @Override
        public void run() {

            for (int i = 0; i < 10000; i++) {
                incrementalData.increment();
            }

        }
    }

    public static class IncrementalData {

        private int value = 0;

        // The critical section
        public void  increment() {
            // Read current value and add 1 to it
            this.value = value + 1;
        }

        public int getValue() {
            return value;
        }

    }
}

Code Analysis:

  • Each time we run this we will get different results. (We have 10 threads each incrementing value 1000 times = result should be 100,000)
  • In between the execution, thread-X and thread-Y both read the current value, lets say 700, into their respective temporary/local variable.
  • And Both Adds 1 to it, resulting in thread-X: 701, thread-Y: 701, resulting into 701. (however it should have been 702).

/java-concurrency-the-complete-basics-part2/racecondition.png
Overlapped access to read/write data

  • This flaw during the middle of execution when single/multiple overlapped happened, affected our final result

  • The flaw is the critical section that’s causing this problem; the “increment()” method - it reads the current value and adds 1 to it.

  • This section must be protected, we will analyze those approaches on section below “Preventing Race Conditions”.

A race condition happened here due to the flaw that occurred when the timing of two or more threads overlapped and affected the correctness. (As seen in above code)

A data race happens when there are two memory accesses in a program where both target the same location. According to the Java Memory Model (JMM), an execution is said to contain a data race if it contains at least two conflicting accesses (reads of or writes to the same variable) that are not ordered by a happens-before relationship.
[See: JVM happens-before relationship ]

A race condition and data race is a considerable overlap: i.e many race conditions are due to data races, and many data races lead to race conditions. On the other hand, we can have race conditions without data races and data races without race conditions.

Preventing Race Conditions

To prevent race conditions from occurring, we must first identify the critical section on the code and avoid it, using thread synchronization. Thread synchronization makes concurrent thread wait until the current working thread comes out of the critical section.

Thread synchronization can be done in java using Synchronized block, locks or atomic variables.

1. Using Synchronized block

1
2
3
4
5
6
7
8
9
// 1. synchronized block on this
synchronized(this) {
  this.value = value + 1; 
}

// 2. synchronized method:
public synchronized void increment() {
  this.value = value + 1; 
}

Following blocks of code (synchronized method, synchronized block) are practically equivalent (even though the bytecode seems to be different).

“Synchronized method uses current object lock and static method uses class object lock”. And also for static methods, the following code has the same effect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class MyClass {
    public static void bar() {
        synchronized(MyClass.class) {
            doSomeOtherStuff();
        }
    }
}
// For static synchronized method these two blocks has same effect
class MyClass {
    public static synchronized void bar() {
        doSomeOtherStuff();
    }
}

Synchronized lock is a Intrinsic locks and is reentrant in nature. Lets discuss about Locks types, Intrinsic locks and reentrant characterstics.

Implicit vs Explicit Locks

All “synchronized locks or monitor locks or intrinsic lock” using the keyword synchronized are “implicit locks”, whereas “explicit locks” are specified by the Lock interface.

Implicit locks implement the reentrant characteristics. Reentrant means that the thread can re-enter another synchronized block on the same object. (i.e from inside a synchronized method it can call another synchronized method on the same object).

This means that if a thread attempts to acquire a lock it already owns, it will not block and it will successfully acquire it. For instance, the following code will not block when called bar() from inside foo(). i.e foo() { bar() }:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void bar(){
    synchronized(this){
        ...
    }
}
// 
public void foo(){
    synchronized(this){
        bar();
    }
}

Explicit locks support more control and thus are expressive. Some of the commonly used explicit locks on java are: ObjectLock, ReentrantLock, ReadWriteLock, StampedLock etc. About Explicit Locks see below: Object Locks

Code Sample Synchronized methods
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
     * Critical section - using "synchronized" Only 1 thread can inter the section at a time
     * i.e only one thread from the instance of "calcService" can execute "synchronized" section
     * 
     * if calcService.incrementValue() is ongoing, another thread can't do calcService.incrementValue or calcService.decrementValue()
     */
    public static class CalcService {

        private int value;

        public void incrementValue() {
            synchronized (this) {
                this.value = value + 1;
            }
        }

        public void decrementValue() {
            synchronized (this) {
                this.value = value - 1;
            }
        }
    }

If a class contains multiple synchronized methods, then one method is locked, all other methods will also be locked for that instance too. i.e instance “calcService1.incrementMethod1()", executing synchronized block, can’t do calcService1.incrementMethod2() at the same time. It will wait for “calcService1” to release its lock.

Internally Java uses a so-called monitor also known as monitor lock or intrinsic lock in order to manage synchronization. This monitor is bound to an object, e.g. when using synchronized methods each method shares the same monitor of the corresponding object. So, it was the monitor that prevented the object from calling another method at the same time in case of the example we demonstrated above for “multiple synchronized methods”.

2. Using Atomic Operations

Atomic operations are safe operations providing non-blocking variable change for multiple threads. These variable uses low-level atomic machine instructions such as compare-and-swap (CAS), to ensure data integrity.
Some built in Java atomic operations are:

  • Reference assignment (a=b)
  • Primitive types READ (except- long, double)
  • volatile keyword
  • Other AtomicVariables ( “java.util.concurrent.atomic”)
    • AtomicInteger, AtomicBoolean… etc
Using volatile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

public class MyRunnable implements Runnable {
    // Volatile means read the most recent value from memory, not locally cached one.
    private volatile boolean active;

    public void run() {
        active = true;
        while(active){
            // some code,
        }
    }

    public void stop(){
        active = false;
    }
}

Using volatile vs Synchronized:

  • Use Volatile when you variables are going to get read by multiple threads, but written to by only one thread.
  • Use Synchronized when your variables will get read and written to by multiple threads.

/java-concurrency-the-complete-basics-part2/AtomicVariable.png
Not thread safe vs synchronized vs atomic variable

3. Using Object Locks

We understood the implicit lock on the section above - Implicit vs Explicit Locks.

Object locks are explicit locks, with more control and expressiveness. some of the commonly used explicit locks in java are ( ObjectLock, ReentrantLock, ReadWriteLock …).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import java.util.ArrayList;
import java.util.List;

public class A3_SynchronizationLocks {

    public static void main(String[] args) throws InterruptedException {
        List<Thread> threads = new ArrayList<>();

        // GenericSavingFund instance for Client Bank-A
        GenericSavingFund arcSavingFund = new GenericSavingFund();
        GenericSavingFund flSavingFund = new GenericSavingFund();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new FundingClients(arcSavingFund));
            thread.setName("Thread ArcSavingFund");
            thread.start();

            Thread thread2 = new Thread(new FundingClients(flSavingFund));
            thread2.setName("Thread FLSavingFund");
            thread2.start();

            threads.add(thread);
        }


        // Wait until all threads are finished
        for (Thread thread : threads) {
            thread.join();
        }

        // Lets Check the final result
        arcSavingFund.getDetails();
        System.out.println("------------");
        flSavingFund.getDetails();
    }

    public static class FundingClients implements Runnable {

        private GenericSavingFund genericSavingFund;

        public FundingClients(GenericSavingFund genericSavingFund) {
            this.genericSavingFund = genericSavingFund;
        }

        @Override
        public void run() {

            for (int i = 0; i < 10000; i++) {
                genericSavingFund.increaseSavingBalance();
                double random = Math.random();
                if( random > 0.95){
                    genericSavingFund.increaseMaturity();
                }
            }

        }
    }

    public static class GenericSavingFund {

        private int savingBalance = 0;
        private int maturityYears = 1;
        private static final int POLICY_MAX_MATURITY_YEARS = 25;

        private Object lock1 = new Object();
        private Object lock2 = new Object();

        public void increaseSavingBalance() {
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + "Increasing current balance from " + savingBalance);
                savingBalance = savingBalance + 5;
            }
        }

        public void increaseMaturity() {
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + "Increasing current maturity from "+ maturityYears);
                if (maturityYears < POLICY_MAX_MATURITY_YEARS) {
                    maturityYears++;
                }
            }
        }

        public void getDetails() {
            System.out.println("Total Max maturity Year: " + POLICY_MAX_MATURITY_YEARS);
            System.out.println("You completed " + maturityYears + " maturity years");
            long remainingYears = (long) POLICY_MAX_MATURITY_YEARS - maturityYears;
            String response = remainingYears==0 ? "Your balance is ready:" + savingBalance : "Please wait some years: " + remainingYears;
            System.out.println(response);
        }

    }
}

1
2
3
4
#output
Total Max maturity Year: 25
You completed 25 maturity years
Your balance is ready: 500000
  1. Let’s say “GenericSavingFund” had made their critical section thread safe; using synchronization.

    • Any number of clients can “increaseSavingBalance()” by 2 freely , or Add 1 year to maturity.
  2. For now let’s assume, we have Two clients who will create their own Saving Fund - Bank A SavingFund, Bank B SavingFund.

  3. Each of them will create GenericSavingFund instance Bank-A: “arcSavingFund” , Bank-B: “flSavingFund”

  4. Problem without synchronized block, in the middle of execution,

    • lets say - balance = 5.0, years = 2
    • Now, 1-“arcSavingFund” thread comes in and reads balance 5.0, and 2- “arcSavingFund” another thread comes in reads balance 5.0,
    • add money to fund, final=7
    • However 2 clients added money into the fund, it should have been 9 (since can add 2 freely).
  5. Critical section here is - “increaseSavingBalance()” - that reads and updates value

  6. Here, increaseSavingBalance() and increaseMaturity() are two independent task

    • Previously, the same object cant call both synchronized methods at the same time,
    • i.e arcSavingFund.increaseSavingBalance() and arcSavingFund.increaseMaturity()
  7. Since this two methods are independent tasks,

    • And we see they operate on different resources - one is savingBalance another maturityYears.
    • For this, we have used diffrent locks, as you saw in the code
    • i.e arcSavingFund.increaseSavingBalance() and arcSavingFund.increaseMaturity() is possible now, using two different locks

Since, now we understand the very basics of using synchronized blocks and locks for protecting our critical section. Sometimes, trying to protect these critical sections, thread needs to wait for the lock to be released by other threads and vice versa, and they depend on each-other resulting into Deadlock.

The deadlock is now introduced here, since it deeply relates to threads locks and waits on the critical section.

Deadlock:

Deadlock can occur when multiple threads need the same locks, at the same time, but obtain them in different order. I.e When thread locks resources, that other thread needs and the thread is trying to acquire lock on another thread creating deadlock.
[See: conditions for deadlock]

ReentrantLock:
ReentrantLock is a mutual exclusion lock with the same basic behavior as the implicit monitors and comes with extended capabilities. This lock implements reentrant characteristics as implicit locks.

ReadWriteLock:
The interface ReadWriteLock maintains a pair of locks for read and write access. Read-lock can be held simultaneously by multiple threads as long as no threads hold the write-lock. This can improve performance and throughput in case that reads are more frequent than writes.
[See: ReentrantLock fairness, tryLock(), ReaderWriter Problem, Dining Philosopher problem]

Semaphore

Semaphore is used for restricting the number of access to resources, using sets of permits. Its core functionality includes acquire() and release() method, useful in scenarios where we have to limit the amount of concurrent access.

1
2
3
4
5
6
7
8
// Semaphore semaphore = new Semaphore (No. of available permits);
Semaphore semaphore = new Semaphore (1); // Binary Semaphore

// acquiring the permit / lock 
semaphore.acquire(); 

// releasing the permit / lock
sesemaphorem.release(); 

Semaphore doesn’t have a notion of owner thread, so it’s not reentrant in nature.
[See: Producer-Consumer Problem using wait()-notify() And also using Semaphore]

Checkpoint
Okay ! Great. Let’s summarize what we have learned here. We understood - Java Memory regions, places for data and places for thread execution, Critical section, Race condition and Data race, JVM happens-before relationship, ways of preventing race condition, using implicit vs explicit locks OR synchronized vs object locks, Atomic operations, Volatile vs Synchronized, Commonly used Object locks, Condition for Deadlock, ReadWriteLocks, Semaphore, Producer-Consumer Problem …. and much more concepts you have explored.

From the previous part 1 - We learned the basic concepts and thread creation. On this part 2 - We learn the details on Data and Resource sharing in multithreaded environment.
On the next part - We are going to learn Thread Management and Scheduling service provided by Java.

Part 3 - Coming Soon….

Thread Pooling, Executor and Schedular in Java.. coming soon