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 returnstrue
. If the lock is not acquired, then returnsfalse
. Unlike thelock()
method, it does not wait for a lock to be obtained if one is not available.void unlock()
: removes the lock.Condition newCondition()
: returns aCondition
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 thesignal/signalAll
methods. Much like thewait
method of the Object classsignal
: signals that a thread that had previously calledawait()
can continue running. The use is similar to using the notify method of theObject
class.signalAll
: signals to all threads that previously hadawait()
called that they can continue. Similar to thenotifyAll()
method of theObject
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.
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.