In the Java platform, synchronization is the capability to control access to shared resources that can be shared between threads or processes. We will talk about threads synchronization because it’s a more common task. Threads can access the same fields and the object’s references. Without correct synchronization is can cause thread interference, memory consistency errors or unexpected behavior of your application.
Let’s look at an example. It will help us to understand the problem without synchronization. Assuming that we have a user with a bank account. There are two transactions on that account with different duration. We are starting the longer transaction. Right after, we are calling the shorter one.
public class SynchronizationExample { public static void main(String[] args){ Account account = new Account(); Thread thread1 = new Transaction(account, 3000); Thread thread2 = new Transaction(account, 1000); thread1.start(); thread2.start(); } } class Transaction extends Thread { private int timeToWait; private Account account; public Transaction(Account account,int timeToWait) { this.timeToWait = timeToWait; this.account = account; } public void run() { try { account.performTransaction(timeToWait); } catch (InterruptedException e) { e.printStackTrace(); } } } class Account { public void performTransaction(int timeToWait) throws InterruptedException { System.out.println("Before transaction " + Thread.currentThread().getId()); Thread.sleep(timeToWait); System.out.println("After transaction " + Thread.currentThread().getId()); } }
The output of this application will be:
Before transaction 12 Before transaction 13 After transaction 13 After transaction 12
The output is inconsistent. The first transaction wasn’t finished, but the second already started. It can cause some problems for a bank client, who wants to know the real amount of money on his account after each transaction.
Synchronization is built on locks or monitors. A thread that needs consistent access has to acquire the lock before accessing them and release the lock when it’s done with them.
The Java platform allows us to do synchronization in different ways. It can be a synchronized method, synchronized block or static synchronization.
We have to mention that constructor cannot be marked as synchronized. It will cause a syntax error because doesn’t have sense. Only the thread that creates that object should have access to its creation.Let’s mark our method performTransaction (int timeToWait) as synchronized. After this, our output will be:
Before transaction 12 After transaction 12 Before transaction 13 After transaction 13
This means that two transactions were done one after one. The first thread locked the method performTransaction (int timeToWait) until finished his work.
Let’s use the synchronized block and get the same result:
class Account { public void performTransaction(int timeToWait) throws InterruptedException { synchronized (this) { System.out.println("Before transaction " + Thread.currentThread().getId()); Thread.sleep(timeToWait); System.out.println("After transaction " + Thread.currentThread().getId()); } } }
Here we specified the object for synchronized statements. That object provides the intrinsic lock. The scope of the synchronized block is usually smaller than the synchronized method scope. We are blocking the whole object. It is better sometimes to synchronize only a part of your method.
Let’s look at a bit different example. We have a value that will be changed by two threads. The example of code without synchronization between threads:
public class SynchronizationExample { public static void main(String[] args) throws InterruptedException { ClientAccount account = new ClientAccount(); System.out.println("Account balance before all transactions is " + account.getBalance()); //transaction to send 100 to account Thread thread1 = new Transaction(account,100,3000); //transaction to get 50 from account Thread thread2 = new Transaction(account,-50,1000); thread1.start(); thread2.start(); } } class Transaction extends Thread { private int timeToWait; private int value; private ClientAccount account; public Transaction(ClientAccount account, int value, int timeToWait) { this.timeToWait = timeToWait; this.value = value; this.account = account; } public void run() { try { account.changeBalance(value, timeToWait); } catch (InterruptedException e) { e.printStackTrace(); } } } class ClientAccount { private Integer balance = 0; public void changeBalance(int value, int time) throws InterruptedException { balance += value; Thread.sleep(time); System.out.println("Account balance after " + value + " is " + balance); } public int getBalance() { return balance; } }
The output will be:
Account balance before all transactions is 0 Account balance after -50 is 50 Account balance after 100 is 50
Our client had balance 0. We started two transactions. The first was to transfer to the client account 100. The second was to transfer from account 50. The application result looks not logical:
0-50 = 50;
50+100 = 50;
The reason is simple. The two treads entered the method changeBalance(int value, int time). The first thread takes more time. That is why the second one raced it. Finally, we got the messy output.
We can to synchronize access of our treads to the balance value. Let’s use the synchronized block for this:
class ClientAccount { private Integer balance = 0; public void changeBalance(int value, int time) throws InterruptedException { synchronized (balance) { balance += value; Thread.sleep(time); System.out.println("Account balance after " + value + " is " + balance); } } ... }
Now, our output is consistent and understandable:
Account balance before all transactions is 0 Account balance after 100 is 100 Account balance after -50 is 50
The syntax of the synchronized block is: synchronized (object reference expression) { //code block }
It is important to note that object reference expression has to be an Object, we cannot use primitive types. That is why we used the object wrapper for int in our example. You will get a java.lang.NullPointerException, in case object reference expression of a synchronized block is null.
We can create a synchronized class. In this case, all methods of this class will be declared as synchronized. For example, Vector, Stack, Hashtable, StringBuffer. They are not recommended for use. The reason is that they are slow.
Static synchronization is when you use synchronization for a static method. In this case, the all-class will be locked, not just object.
public class StaticSynchExample { public static void main(String[] args){ Thread thread1 = new Printer(1); Thread thread2 = new Printer(2); thread1.start(); thread2.start(); } } class Printer extends Thread { private int k; public Printer(int k) { this.k = k; } public void run() { try { Document.print(k); } catch (InterruptedException e) { e.printStackTrace(); } } } class Document { private static int K = 1; public static synchronized void print(int k) throws InterruptedException { K = k; System.out.print("Start"); for(int i = 0; i < 5; i++) { System.out.print(i*K + " "); } System.out.println("End"); } }
The output will be not logical. The threads accessed and changed our coefficient randomly:
Start Start 0 0 2 4 6 8 End 2 4 6 8 End
After marking our print(int k) method as synchronized, we will get consistent result:
Start 0 1 2 3 4 End Start 0 2 4 6 8 End
Let’s summarize the important things to know about the synchronized keyword. Synchronization guarantees that two threads cannot execute a synchronized method with the same lock simultaneously or concurrently. The synchronized keyword can be used for methods or blocks (synchronized statements). When a thread enters a synchronized part of code, the code acquires a lock. This lock exists until the thread leaves that code or an error/exception occurred.
A thread acquires an object level lock when enters an instance synchronized method. A thread acquires a class level lock when enters a static synchronized method.
The synchronized keyword is re-entrant. Let’s explain it. A thread that holds a lock on one synchronized method does not need to acquire a lock for a synchronized method called from the first method.
There is one main disadvantage of Java synchronized keyword. It doesn’t allow concurrent read. This limits scalability. You can use ReentrantReadWriteLock for this use case. It is from package java.util.concurrent.locks. It provides a ready implementation of the ReadWriteLock in Java.
The synchronized keyword can control access to a shared object just within the same JVM.
A synchronized method in Java is very slow. They degrade performance. So, it better to use blocks for critical sections of code.
Your synchronized code could cause a java deadlock or starvation while accessing by multiple threads if synchronization is not implemented correctly.
It’s not good to use a non-final field or String on the synchronized block. A string is an immutable object. A literal string and interned string gets stored in the String pool. Another thread can be locked on the same object despite being completely unrelated. It can cause unexpected behavior.
Besides the synchronized keyword, we can also use explicit locks. The Java language includes the Lock interface since version 1.5. There are different implementations of this interface (ReentrantLock, ReadWriteLock, StampedLock, etc ). Let’s take a look at the simplest ReentrantLock, just to replace our synchronized keyword from our first example. We will replace this part:
class Account { public void performTransaction(int timeToWait) throws InterruptedException { synchronized (this) { System.out.println("Before transaction " + Thread.currentThread().getId()); Thread.sleep(timeToWait); System.out.println("After transaction " + Thread.currentThread().getId()); } } } This code work the same as: class Account { private ReentrantLock lock = new ReentrantLock(); public void performTransaction(int timeToWait) throws InterruptedException { lock.lock(); System.out.println("Before transaction " + Thread.currentThread().getId()); Thread.sleep(timeToWait); System.out.println("After transaction " + Thread.currentThread().getId()); lock.unlock(); } }
One Response