An Introduction To Concurrency and Synchronization In Java

ERP Solutions oodles
6 min readJan 15, 2021

--

Synchronization :

In our previous blog post of Concurrency, we’ve learned how to execute code in parallel via executor services. When writing such multi-threaded code you have to pay particular attention when accessing shared mutable variables simultaneously from multiple threads.

For example, we want to increment an integer number which is accessed by multiple threads simultaneously.

The sleep and stop methods used in examples :

public static void stop(ExecutorService es) {

try {

es.shutdown();

es.awaitTermination(60, TimeUnit.SECONDS);

}

catch (InterruptedException exception) {

System.out.println(“Terminitation Interrupt!!”);

}finally {

if(!es.isTerminated()) {

System.out.println(“terminatine non finished tasks”);

}

es.shutdownNow();

}

}

public static void sleep(int seconds) {

try {

TimeUnit.SECONDS.sleep(seconds);

}

catch (InterruptedException exception) {

throw new IllegalStateException(exception);

}

}

int number = 0;

void increment() {

number ++;

}

We will be in serious trouble when calling this method concurrently from different threads.

ExecutorService es = Executors.newFixedThreadPool(2);

IntStream.range(0, 10000)

.forEach(i -> es.submit(this::increment));

stop(es);

System.out.println(number); // 9965

Instead of seeing a constant result count of 10000 the actual result varies every time we execute the code. This is because we have shared a mutable variable over multiple threads without even synchronizing the access to this variable which results in a race condition.

In order to increment the the number, three steps needs to be performed:

(i) read the current value,

(ii) increase this value by one

(iii) assign the new value to the variable.

If two or more threads perform these steps in parallel, it is possible that both threads execute step 1 at the same time, thus reading the same current value. This results in lost some write operations, so the actual result is lower. In the above sample 35 increments got lost due to concurrent unsynchronized access to number count but you may see different results when executing the code by yourself.

Fortunately, Java supports thread synchronization since the early versions by using the synchronized keyword. We can synchronize the method which is increment the value :

synchronized void increment() {

number ++;

}

When using synchronized keywords we get the desired output every time we execute the code.

ExecutorService es = Executors.newFixedThreadPool(2);

IntStream.range(0, 10000)

.forEach(i -> es.submit(this::increment));

stop(es);

System.out.println(count); // 10000

Synchronized keyword can be use for block statement:

synchronized void increment() {

synchronized(this){

number ++;

}

}

In Order to manage synchronization, Java uses monitor lock or intrinsic lock internally.

Locks :

The Concurrent API supports various explicit locks specified by lock interface.

Locks support various methods and are more expressive than implicit locks.

Lock Implementations :

1. ReentrantLock :

ReentrantLock allows threads to enter into lock on a resource more than once.

When the thread enters into lock for the first time, a hold count is set to 1, whenever a thread is re entered into lock the count is incremented by 1. The count is decremented in every unlock request. when the count is 0, the resource is unlocked.

Example of ReenteantnLock,

ReentrantLock lock = new ReentrantLock();

int count = 0;

void increment() {

lock.lock();

try {

count++;

} finally {

lock.unlock();

}

}

lock() : acquires the lock and unlock() releases the lock.

Various methods supported by locks to fine grained the control :

ExecutorService es = Executors.newFixedThreadPool(2);

ReentrantLock lock = new ReentrantLock();

es.submit(() -> {

lock.lock();

try {

sleep(1);

} finally {

lock.unlock();

}

});

es.submit(() -> {

System.out.println(“Locked: “ + lock.isLocked());

System.out.println(“Held by me: “ + lock.isHeldByCurrentThread());

boolean locked = lock.tryLock();

System.out.println(“Lock acquired: “ + locked);

});

stop(es);

=== OUTPUT ===

Locked: true

Held by me: false

Lock acquired: false

The method tryLock(), an alternative to lock() tries to acquire the lock without pausing the current thread.

The returned boolean can be used to check whether the lock has been acquired before accessing any shared mutable variables or not.

ReadWriteLock:

The interface ReadWriteLock maintains a pair of locks for read/write access.

The idea behind read/write locks is that it is usually safe to read mutable variables concurrently as long as no other thread is writing to this variable. So the read lock can be held by multiple threads at the same time as long as no threads hold the write lock.

ExecutorService es = Executors.newFixedThreadPool(2);

Map<String, String> map = new HashMap<>();

ReadWriteLock lock = new ReentrantReadWriteLock();

es.submit(() -> {

lock.writeLock().lock();

try {

sleep(1);

map.put(“foo”, “bar”);

} finally {

lock.writeLock().unlock();

}

});

The above example first acquires the write lock in order to add value to the map after sleeping for 1 second.

Runnable readTask = () -> {

lock.readLock().lock();

try {

System.out.println(map.get(“foo”));

sleep(1);

} finally {

lock.readLock().unlock();

}

};

es.submit(readTask);

es.submit(readTask);

stop(es);

After executing this code we can notice that both read tasks have to wait for the Write task to finish.

After the write lock has been released both the read will be executed simultaneously and print the result to the console. They don’t have to wait for each other to finish, because read locks can safely be acquired at the same time as long as no write lock is held by some other thread.

StampedLock :

StampedLock also supports read/ write locks. In comparison to ReadWriteLock the locking methods of a StampedLock return a stamp , a long value. These stamps can be used to release a lock or to check if the lock is still valid. Additionally stamped locks support another lock mode i.e. optimistic locking.

Example :

ExecutorService es = Executors.newFixedThreadPool(2);

Map<String, String> map = new HashMap<>();

StampedLock lock = new StampedLock();

es.submit(() -> {

long stamp = lock.writeLock();

try {

sleep(1);

map.put(“foo”, “bar”);

} finally {

lock.unlockWrite(stamp);

}

});

Runnable readTask = () -> {

long stamp = lock.readLock();

try {

System.out.println(map.get(“foo”));

sleep(1);

} finally {

lock.unlockRead(stamp);

}

};

es.submit(readTask);

es.submit(readTask);

stop(es);

Acquiring a read or write lock via readLock() or writeLock() returns a stamp, which is later can be used for unlocking in the finally block. Remember that stamped locks don’t implement reentrant characteristics. Each call to lock will return a new stamp and block if no lock is available even if the same thread already holds a lock. So you have to be careful not to run into deadlocks.

ExecutorService es = Executors.newFixedThreadPool(2);

StampedLock sLock = new StampedLock();

es.submit(() -> {

long stamp = sLock.tryOptimisticRead();

try {

System.out.println(“OptimisticLock Validate: “ + sLock.validate(stamp));

sleep(1);

System.out.println(“OptimisticLock Validate: “ + sLock.validate(stamp));

sleep(2);

System.out.println(“OptimisticLock Validate: “ + sLock.validate(stamp));

} finally {

sLock.unlock(stamp);

}

});

es.submit(() -> {

long stamp = sLock.writeLock();

try {

System.out.println(“Write Lock is acquired”);

sleep(2);

} finally {

sLock.unlock(stamp);

System.out.println(“Write done. . .”);

}

});

stop(es);

Let’s write the last example code again to use StampedLock instead of ReadWriteLock:

ExecutorService es = Executors.newFixedThreadPool(2);

StampedLock lock = new StampedLock();

es.submit(() -> {

long stamp = lock.tryOptimisticRead();

try {

System.out.println(“Optimistic Lock Valid: “ + lock.validate(stamp));

sleep(1);

System.out.println(“Optimistic Lock Valid: “ + lock.validate(stamp));

sleep(2);

System.out.println(“Optimistic Lock Valid: “ + lock.validate(stamp));

} finally {

lock.unlock(stamp);

}

});

es.submit(() -> {

long stamp = lock.writeLock();

try {

System.out.println(“Write Lock acquired”);

sleep(2);

} finally {

lock.unlock(stamp);

System.out.println(“Write done”);

}

});

stop(es);

You can acquire an OptimisticLock by calling tryOptimisticRead() method which returns a stamp without blocking the current thread, no matter if the lock is actually available. If there is already a write lock active the returned stamp equals to 0. You can call lock.validate(stamp) to check if a stamp is valid or not.

Answer :

Optimistic Lock Valid: true

Write Lock acquired

Optimistic Lock Valid: false

Write done

Optimistic Lock Valid: false

At Oodles, we provide complete ERP development and integration solutions to drive organizational growth and streamline inbound/outbound processes. Our team uses open-source tools and ERP platforms to custom build feature-rich enterprise applications from the ground up to address varied business needs. For more information, visit erpsolutions.oodles.io.

--

--

ERP Solutions oodles
ERP Solutions oodles

Written by ERP Solutions oodles

We are a leading ERP development company in India offers a wide range of ERP software development services to help you grow your business exponentially.

No responses yet