Java | Multithreading Part 4: Synchronization

MrAndroid
10 min readDec 14, 2022

--

Synchronization — is the capability to control the access of multiple threads to any shared resource.

Multithreaded programs may often come to a situation where multiple threads try to access the same shared resources.

Synchronization helps prevent non-deterministic state of an object.

Every object in Java has a Monitor with which a thread can synchronize. That is, acquire a lock that guarantees that no other thread will access this object until the lock is released.

Monitor:

Monitor, mutex— it is a means of providing control over access to a resource. A monitor can have a maximum of one owner at any given time. Therefore, if someone is using a resource and has seized a monitor for sole access, then another who wants to use the same resource must wait for the monitor to be released, seize it, and only then start using the resource.

How we check if a thread is holding the monitor of a particular resource:

Thread.holdsLock(lock);

Synchronization:

Synchronization is classified into two types: Process synchronization and Thread synchronization.

Process synchronization — means that any processes share system resources and handle concurrent access to shared data, minimizing the chance of data inconsistencies.

Thread synchronization — means that when one thread starts working with shared data, no other thread is allowed access until the first thread is done.

Thread synchronization synchronization:

Mutual Exclusive:
A Mutex, also known as a Mutual Exclusive, allows only one thread to access shared resources. It will not enable simultaneous access to shared resources. It is possible to accomplish this in the following methods.

  • Synchronized Method
  • Synchronized block
  • Static Synchronization

Synchronized Method:

If one thread enters a block of code that is marked with the word synchronized, it instantly captures the object’s mutex, and all other threads that try to enter the same block or method are forced to wait until the previous thread completes its work and releases the monitor.

public synchronized void objLock() throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

Synchronized block:

Let’s say you don’t want to synchronize the whole method, you want to synchronize multiple lines of code in a method, then a synchronized block helps keep those few lines of code in sync.

public void lock() throws InterruptedException {
for (int i = 0; i < 3; i++) {
synchronized (this) {
Thread.sleep(500);
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

The synchronized block will take an object as a parameter, the given object will work just like a synchronous method. In the case of a synchronized method, the lock is accessed on the method, but in the case of a synchronized block, the lock is accessed on the object.ту.

Static Synchronization:

If the method that contains the critical “threaded” logic is static, synchronization will be done by class.

public static synchronized void staticLock() throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

The use of synchronized static is locking at the class level, while synchronized is locking at the instance level.

A synchronized static method does't have access to this, but it has access to an object of the Class class, it is present in a single instance and it is he who acts as a monitor for synchronizing static methods.

So the following construction is equivalent:

public class SomeClass {

public static synchronized void someMethod() {
...
// code
}
}

// is equivalent to this:

public class SomeClass {

public static void someMethod(){
synchronized(SomeClass.class) {
...
// code
}
}
}

Inter-thread Communication:

This is another way to implement synchronization.

Inter-thread communication controls the communication between the synchronized threads.

This type of synchronization allows another foreground thread to enter the critical section, suspending the execution of the current thread. For this, the interaction between threads is very important, otherwise it can lead to critical problems.

Inter-thread synchronization is done using wait()/notify().

The thread calls the wait() method on the object, having previously captured its monitor. This stops his work.

Another thread can call the notify() method on the same object (again, having previously captured the object’s monitor), as a result of which the thread waiting on the object “wakes up” and continues its execution.

The wait()/notify() methods should be called explicitly, via a synchronized block.

If we don’t call wait() or notify() method from synchronized context, we will get IllegalMonitorStateException in Java.

Race conditions can exist between wait() and notify() if we don’t call them inside a synchronized method or block.

Lock:

Lock is an alternative and more flexible thread synchronization mechanism compared to the basic: synchronized, wait, notify, notifyAll.

Lock classes implement the Lock interface, which defines the following methods:

  • void lock(): acquires the lock if it is available. If the lock is unavailable, the current thread becomes disabled until the lock has been acquired.
  • void lockInterruptibly() throws InterruptedException:acquires the lock if it is available, if the thread isn't interrupted.
  • boolean tryLock(): try to acquire the lock, if the lock is acquired, then returns true. If the lock is not acquired, then returns false. Unlike the lock() method, it does not wait for a lock to be obtained if one is not available.
  • void unlock(): removes the lock.
  • Condition newCondition(): returns a Condition object that is associated with the current lock.

As a rule, the ReentrantLock class is used to work with the lock.

For example:

Let’s write a synchronized Stack using ReentrantLock for synchronization.

The stack has a maximum size of 3 elements.

If our stack is full, then the thread that wants to add a new element will wait until the buffer is freed. If the buffer is empty, then the thread that wants to take the element will wait until a new element appears.

public class ConcurrentStackWithLocker<T> {

private final Integer bufferSize;

private static final int BUFFER_MAX_SIZE = 3;

protected Object[] items;
protected volatile int lastPosition;

ReentrantLock locker;
private final Condition waitPop;
private final Condition waitPush;

public ConcurrentStackWithLocker() {
this(BUFFER_MAX_SIZE);
}

public ConcurrentStackWithLocker(Integer bufferSize) {
this.bufferSize = bufferSize;
items = new Object[bufferSize];

locker = new ReentrantLock();
waitPop = locker.newCondition();
waitPush = locker.newCondition();
}

public boolean empty() {
return lastPosition == 0;
}

private boolean isFull() {
return lastPosition == bufferSize;
}

public T push(T element) throws InterruptedException {
try {
locker.lock();

while (isFull()) {
waitPop.await();
}

System.out.println("push index=" + lastPosition + " element=" + element);

items[lastPosition++] = element;
System.out.println("after push index=" + lastPosition + " element=" + element);

waitPush.signalAll();
} finally {
locker.unlock();
}

return element;
}

public T pop() throws InterruptedException {
try {
locker.lock();

while (empty()) {
waitPush.await();
}

T item = peek();

System.out.println("pop index=" + lastPosition + " item=" + item);

lastPosition--;
items[lastPosition] = null;
System.out.println("after pop index=" + lastPosition + " item=" + item);

waitPop.signalAll();

return item;
} finally {
locker.unlock();
}
}

public T peek() {
int index = lastPosition - 1;
return (T) items[index];
}

public void display() {
System.out.print("[");
for (int j = 0; j < items.length; j++) {
System.out.print(items[j] + " ");
}
System.out.println("]");
}
}

Run our code:

public static void main(String[] args) {
ConcurrentStackWithLocker stack = new ConcurrentStackWithLocker<Integer>();

new Thread(() -> {
try {
Thread.sleep(300);
stack.push("1");
stack.push("2");
Thread.sleep(10);
stack.push("3");
stack.push("4");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

new Thread(() -> {
try {
stack.pop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

new Thread(() -> {
try {
Thread.sleep(2000);
stack.display();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}

in console:

push index=0 element=1
after push index=1 element=1
push index=1 element=2
after push index=2 element=2
pop index=2 item=2
after pop index=1 item=2
push index=1 element=3
after push index=2 element=3
push index=2 element=4
after push index=3 element=4
[1 3 4 ]

How it work:

After we called: locker.lock();

Only one thread has access to the critical section, while other threads wait for the lock to be released: locker.unlock();

Since we use Lock for synchronization, we create a new ReentrantLock object.

In order to report that it is possible to add or remove an element while the thread is waiting, use the newCondition() method.

locker = new ReentrantLock();
waitPop = locker.newCondition();
waitPush = locker.newCondition();

When the push() method is called, we check if our buffer is full, if it is full, then we put our thread in wait mode:

while (isFull()) {
waitPop.await();
}

In the pop() method, when we have removed an element, it notifies all threads that are waiting that the element has been removed and can be added:

waitPop.signalAll();

Condition:

The behavior of Condition objects is much the same as using the wait/notify/notifyAll methods in the Object class.

We can use the following methods of the Condition interface:

  • await: a thread waits until some condition is met and until another thread calls the signal/signalAll methods. Much like the wait method of the Object class
  • signal: signals that a thread that had previously called await() can continue running. The use is similar to using the notify method of the Object class.
  • signalAll: signals to all threads that previously had await() called that they can continue. Similar to the notifyAll() method of the Object class

These methods are called from within a block of code that is affected by a ReentrantLock.

First, using this lock, we need to get the Condition object:

ReentrantLock locker = new ReentrantLock();
Condition condition = locker.newCondition();

As a rule, the access condition is checked first. If the condition is done, then the thread waits until the condition changes:

while (condition)
condition.await();

After all the actions are completed, other threads are signaled that the condition has changed:

condition.signalAll();

ReadWriteLock:

The standard ReadWriteLock interface provides thread-safe shared read and write access. For these purposes, two methods are declared in it: readLock() and writeLock(). They return objects under the Lock interface.

Both types of locks on the same ReadWriteLock instance are related. Until some thread takes the write lock, as many threads as they want can read without interfering with each other. The readLock lock closes a part of the code with the “read-only” semantics of some conditional “resource”. In the critical section of the writeLock code, the resource is modified.

The properties of these locks protect the program from resource concurrent writes and reads during writes.

Possible synchronization problems:

Deadlock:

The situation in which ThreadA is blocking ObjectA, it needs another ObjectB which is blocked by another thread ThreadB.

ThreadB does not release ObjectB because to complete some operation, it needs ObjectA, which is blocked by ThreadA.

It turns out that ThreadA is waiting for ObjectB to be unlocked by ThreadB, which is waiting for ObjectA to be unlocked by ThreadA.

Thus, threads wait for each other. As a result, the whole program “deadlock” and waits for the threads to somehow unblock and continue working.

Deadlock

There can be many threads in a deadlock.

For exemple:

public static void main(String[] args) {
final String objectA = "ObjectA";
final String objectB = "ObjectB";

Thread threadA = new Thread(() -> {
synchronized (objectA) {
System.out.println("ThreadA: Locked objectA");
try {
Thread.sleep(100);
} catch (Exception ignored) {
}
synchronized (objectB) {
System.out.println("ThreadA: Locked objectB");
}
}
});
Thread threadB = new Thread(() -> {
synchronized (objectB) {
System.out.println("ThreadB: Locked objectB");
try {
Thread.sleep(100);
} catch (Exception ignored) {
}
synchronized (objectA) {
System.out.println("ThreadB: Locked objectA");
}
}
});

threadA.start();
threadB.start();
}

Logs :

ThreadA: Locked objectA
ThreadB: Locked objectB

Livelock:

A situation where ThreadA causes ThreadB to perform some action, which in turn causes ThreadA to execute the original action, which again invokes ThreadB’s action.

In essence, Livelock is similar to Deadlock, but the threads do not “Deadlock” on the system wait, but do something forever.

For example:

We have two Persons who want to transfer money. But none of them wants to be the first to translate.

Person1 checks if Person2 has completed the translation. If not, it displays the message: You transfer first and continues to check in a loop until Person2 completes the transfer.

In turn, Person2 performs the same logic.

It turns out a cyclic dependence.

public static void main(String[] args) {
final Person person1 = new Person("Bob");
final Person person2 = new Person("Alice");

final MoneyTransfer s = new MoneyTransfer(person1);

new Thread(() -> person1.moneyTransferWith(s, person2)).start();

new Thread(() -> person2.moneyTransferWith(s, person1)).start();
}

static class MoneyTransfer {
private Person owner;

public MoneyTransfer(Person d) {
owner = d;
}

public synchronized void setOwner(Person d) {
owner = d;
}

public synchronized void transfer() {
System.out.printf("%s transferred money!", owner.name);
}
}

static class Person {
private final String name;
private boolean isNeedTransfer;

public Person(String name) {
this.name = name;
isNeedTransfer = true;
}

public String getName() {
return name;
}

public boolean isNeedTransfer() {
return isNeedTransfer;
}

public void moneyTransferWith(MoneyTransfer transfer, Person person) {
while (isNeedTransfer) {
if (transfer.owner != this) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
continue;
}
continue;
}

if (person.isNeedTransfer()) {
System.out.printf(
"%s: You transfer first %s!%n",
name, person.getName());
transfer.setOwner(person);
continue;
}

transfer.transfer();
isNeedTransfer = false;
System.out.printf(
"%s: I am transferred money %s!%n",
name, person.getName());
transfer.setOwner(person);
}
}
}
Alice: You transfer first Bob!
Bob: You transfer first Alice!

Conclusions:

Synchronization is an option when you want only one thread to access the shared resource in one time moment.

But synchronization can introduce thread contention, which occurs when two or more threads try to access the same resource simultaneously and cause the Java runtime to execute one or more threads more slowly, or even suspend their execution. Starvation and livelock are forms of thread contention.

The Lock API provides more options for locking, unlike synchronized, where a thread can wait indefinitely for a lock.

Synchronized code is cleaner and easier to maintain. In the case of using the Lock API, we are forced to write a try-finally block to make sure that the lock is released.

Synchronization blocks can only cover one method, while the Lock API allows you to get a lock in one method and release it in another.

--

--