Thread Synchronization
With respect to multithreading, Synchronization is a process of controlling the access of shared resources by the multiple threads in such a manner that only one thread can access a particular resource at a time. In non synchronized multithreaded application, it is possible for one thread to modify a shared object while another thread is in the process of using or updating the object’s value.
Here is an example that shows why synchronization is useful. In this case, we have four new threads all work with one shared data object. The object prints out a start and end message, and it completes a half-second task between printing the messages. Actually, the data object should start, perform its task, and end before the new thread starts a new task, but that is not way here – each thread grabs the data object for itself. The code (Synchronize1.java) is given below:
Synchronize1.java
public class Synchronize1 {
public static void main(String args[]) {
Shared shared = new Shared();
MyThread thread1 = new MyThread(shared, "1st");
MyThread thread2 = new MyThread(shared, "2nd");
MyThread thread3 = new MyThread(shared, "3rd");
MyThread thread4 = new MyThread(shared, "4th");
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
}
catch(InterruptedException e) {
System.out.println("Exception: " + e);
}
}
}
class MyThread extends Thread {
Shared shared;
public MyThread(Shared shared, String string) {
super(string);
this.shared = shared;
start();
}
public void run() {
shared.doWork(Thread.currentThread().getName());
}
}
class Shared {
void doWork(String string) {
System.out.println("Starting " + string);
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
System.out.println("Exception: " + e);
}
System.out.println("Ending " + string);
}
}
Output:
Starting 1st
Starting 2nd
Starting 3rd
Starting 4th
Ending 1st
Ending 3rd
Ending 2nd
Ending 4th
Synchronization prevents such type of data corruption which may otherwise lead to dirty reads and significant errors. Generally critical sections of the code are usually marked with synchronized keyword. An example of using thread synchronization is in “The Producer/Consumer Model”. Synchronization is based on the concept of monitor. The monitor controls the way in which a thread can access a shared resource in a synchronized way.
Figure 1: Thread synchronization with monitor
A lock (monitor) is used to synchronize access to a shared resource. A lock can be associated with a shared resource. Threads gain access to a shared resource by first acquiring the lock associated with the object/block of code. At any given time, at most only one thread can hold the lock (i.e., own the monitor) and thereby have access to the shared resource. A lock thus implements mutual exclusion. A lock thus implements mutual exclusion (mutex).
In Java, all objects have a lock—including arrays. This means that the lock from any Java object can be used to implement mutual exclusion. By associating a shared resource with a Java object and its lock, the object can act as a guard, ensuring synchronized access to the resource. Only one thread at a time can access the shared resource guarded by the object lock.
The object lock mechanism enforces the following rules of synchronization:
- A thread must acquire the object lock associated with a shared resource, before it can enter the shared resource. The runtime system ensures that no other thread can enter a shared resource if another thread already holds the object lock associated with the shared resource. If a thread cannot immediately acquire the object lock, it is blocked, that is, it must wait for the lock to become available.
- When a thread exits a shared resource, the runtime system ensures that the object lock is also relinquished. If another thread is waiting for this object lock, it can proceed to acquire the lock in order to gain access to the shared resource.
Classes also have a class-specific lock that is analogous to the object lock. Such a lock is actually a lock on the java.lang.Class object associated with the class. Given a class A, the reference A.class denotes this unique Class object. The class lock can be used in much the same way as an object lock to implement mutual exclusion.
There can be 2 ways through which synchronized can be implemented in Java:
- synchronized blocks of code
- synchronized methods
1) Synchronized Blocks of Code
To synchronize a block of code, we use the synchronize keyword, indicating the object we want to restrict access to. The synchronized block allows execution of arbitrary code to be synchronized on the lock of an arbitrary object.
The general form of the synchronized block is as follows:
synchronized (<object reference expression>) {
<code block>
}
See the example below which has been extended from the previous section:
Synchronize2.java
public class Synchronize2 {
public static void main(String args[]) {
Shared shared = new Shared();
MyThread thread1 = new MyThread(shared, "1st");
MyThread thread2 = new MyThread(shared, "2nd");
MyThread thread3 = new MyThread(shared, "3rd");
MyThread thread4 = new MyThread(shared, "4th");
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
}
catch(InterruptedException e) {
System.out.println("Exception: " + e);
}
}
}
class MyThread extends Thread {
Shared shared;
public MyThread(Shared shared, String string) {
super(string);
this.shared = shared;
start();
}
public void run() {
// Synchronized block of code
synchronized(shared) {
shared.doWork(Thread.currentThread().getName());
}
}
}
class Shared {
void doWork(String string) {
System.out.println("Starting " + string);
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
System.out.println("Exception: " + e);
}
System.out.println("Ending " + string);
}
}
Output:
Starting 1st
Ending 1st
Starting 4th
Ending 4th
Starting 3rd
Ending 3rd
Starting 2nd
Ending 2nd
2) Synchronized Methods
Synchronized methods are methods that are used to control access to an object. A thread only executes a synchronized method after it has acquired the lock for the method’s object or class. If the lock is already held by another thread, the calling thread waits. A thread relinquishes the lock simply by returning from the synchronized method, allowing the next thread waiting for this lock to proceed. Synchronized methods are useful in situations where methods can manipulate the state of an object in ways that can corrupt the state if executed concurrently. This is called a race condition. It occurs when two or more threads simultaneously update the same value, and as a consequence, leave the value in an undefined or inconsistent state. While a thread is inside a synchronized method of an object, all other threads that wish to execute this synchronized method or any other synchronized method of the object will have to wait until it gets the lock. This restriction does not apply to the thread that already has the lock and is executing a synchronized method of the object. Such a method can invoke other synchronized methods of the object without being blocked. The non-synchronized methods of the object can of course be called at any time by any thread. See the example below:
Synchronize3.java
public class Synchronize3 {
public static void main(String args[]) {
Shared shared = new Shared();
MyThread thread1 = new MyThread(shared, "1st");
MyThread thread2 = new MyThread(shared, "2nd");
MyThread thread3 = new MyThread(shared, "3rd");
MyThread thread4 = new MyThread(shared, "4th");
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
}
catch(InterruptedException e) {
System.out.println("Exception: " + e);
}
}
}
class MyThread extends Thread {
Shared shared;
public MyThread(Shared shared, String string)
{
super(string);
this.shared = shared;
start();
}
public void run() {
shared.doWork(Thread.currentThread().getName());
}
}
class Shared {
// Synchronized function
synchronized void doWork(String string) {
System.out.println("Starting " + string);
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
System.out.println("Exception: " + e);
}
System.out.println("Ending " + string);
}
}
Output:
Starting 2nd
Ending 2nd
Starting 3rd
Ending 3rd
Starting 4th
Ending 4th
Starting 1st
Ending 1st
Note: The output sequence may be different from the previous one. As programmers, we don’t have control over the threads. It is the JVM thread scheduler who is responsible for execution of threads. That is why different run of the same program may produce different output sequences in terms of thread execution.