Synchronization of Java
Nevertheless, problems may occur when several threads running in the same application try to access and change common resources (such as files, variables, or network connections). Such simultaneous accesses may result in inconsistent data and unpredictable outcomes if they are not properly coordinated. In order to avoid these issues, synchronization methods are required to guarantee that only one thread can access a shared resource or a crucial portion of code at any given moment.
Why Synchronization is Needed
To avoid race circumstances, synchronization is the main motivation. If the result of a program is dependent on the order or timing of actions taken by several threads on the same data, this is known as a race condition. Threads may interact with one another if they are not coordinated properly, which could result in inaccurate findings or corrupted data. Corruption could result, for instance, if two threads attempt to write to the same file at the same time and overwrite each other’s data.
Let’s illustrate with an example where synchronization is missing:
class Callme {
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000); // Simulate some work/delay
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t.start();
}
public void run() {
target.call(msg); // Calling the shared method without synchronization
}
}
public class SynchDemo {
public static void main(String args[]) {
Callme target = new Callme();
Caller ob1 = new Caller(target, "Hello");
Caller ob2 = new Caller(target, "Synchronized");
Caller ob3 = new Caller(target, "World");
// Wait for threads to end
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
}
Output (example, may vary due to race condition):
[Hello[Synchronized[World]
]
]
Due to call()
ability to move to a different thread in the middle of an operation, the messages are jumbled, as can be seen in the output. Classic race conditions like this one.
Synchronized Keyword
The synchronized
keyword, which Java offers, regulates access to shared resources and avoids race situations. It guarantees that a method or block of code can only be used by one thread at a time for a particular object. The implicit monitor (or lock) that is linked to each Java object is something that threads can acquire and release.
Synchronized Methods
Prior to the method being executed, the thread implicitly obtains the object instance’s monitor when a synchronized
method is specified. No other thread is permitted to enter any other synchronized
method on the same object instance while a thread is inside a synchronized
method. Any subsequent threads that try to do so will be halted until the first thread releases the monitor and leaves the synchronized
method.
Let’s fix the previous example using a synchronized
method:
class Callme {
// Method is now synchronized
synchronized void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
// Caller and SynchDemo classes remain the same as before.
Output (now consistent):
[Hello]
[Synchronized]
[World]
By simply adding synchronized
to the call
method, access to target.call(msg)
is serialized, ensuring that each thread completes its execution within the method before another thread can start.
Synchronized Blocks
Despite their convenience, synchronised methods may be overly general in certain situations, locking down the entire object even when the method is just accessing a small fraction of its information. You can use any object as the monitor to synchronise access to a particular block of code with synchronised blocks, giving you more precise control. You can use this to synchronise access to items from classes that you can’t change, such third-party libraries.
The general form of a synchronized block is: synchronized(objref) { // statements to be synchronized }
. Here, objref
is the object whose monitor will be acquired by the executing thread.
Let’s modify the Caller
class to use a synchronized block:
class Callme {
// call method is NOT synchronized here
void call(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callme target;
Thread t;
public Caller(Callme targ, String s) {
target = targ;
msg = s;
t = new Thread(this);
t.start();
}
public void run() {
// Synchronized block uses 'target' object as the monitor
synchronized(target) {
target.call(msg);
}
}
}
// SynchDemo class remains the same as before.
Output (consistent, similar to synchronized method):
[Hello]
[Synchronized]
[World]
This version achieves the same correct, sequential output because the synchronized(target)
block ensures that only one thread can call target.call(msg)
at a time using target
‘s monitor.
Inter-thread Communication: , , methods
The Java wait()
, notify()
, and notifyAll()
methods are useful for more intricate coordination when threads need to communicate with one another or wait for particular criteria to be met. Because they are defined in the Object
class, all Java objects can use these methods.
Crucially, these methods must only be called from within a synchronized
context (a synchronized
method or a synchronized
block). If called outside a synchronized context, they will throw an IllegalMonitorStateException
.
- wait(): This function instructs the running thread to enter a
WAITING
state and release the monitor it is holding on to the object. The thread stays in this state until a certain amount of time has passed (for timedwait()
versions) or until another thread callsnotify()
ornotifyAll()
on the same object. Awhile
loop that tests the condition the thread is waiting for should callwait()
in order to prevent spurious wakeups, which occur when a thread wakes up without anotify()
call. - notify(): Using the notify() function, one waiting thread on this object’s monitor gets woken up. Java Virtual Machine (JVM) implementation determines the random choice of which waiting thread is awakened.
- notifyAll(): Opens all pending threads on the monitor of this object. Then, one of the threads that was awakened will attempt to regain the lock on the object.
With regard to inter-thread communication, the Producer-Consumer Problem is a famous example. Data is created and added to a shared queue by one thread (the producer), and it is retrieved and processed by another thread (the consumer). To make sure that neither the consumer nor the producer tries to withdraw data from an empty queue or add data to one that is already full, they must work together.
// A correct implementation of a producer and consumer using wait() and notify()
class Q {
int n;
boolean valueSet = false; // Flag to indicate if a value has been "put" and not yet "got"
synchronized int get() {
while(!valueSet) { // Loop to handle spurious wakeups
try {
wait(); // Wait until a value is set by the producer
} catch(InterruptedException e) {
System.out.println("InterruptedException caught");
}
}
System.out.println("Got: " + n);
valueSet = false;
notify(); // Notify the producer that the value has been consumed
return n;
}
synchronized void put(int n) {
while(valueSet) { // Loop to handle spurious wakeups
try {
wait(); // Wait until the consumer gets the current value
} catch(InterruptedException e) {
System.out.println("InterruptedException caught");
}
}
this.n = n;
valueSet = true;
System.out.println("Put: " + n);
notify(); // Notify the consumer that a new value is available
}
}
class Producer implements Runnable {
Q q;
Producer(Q q) {
this.q = q;
new Thread(this, "Producer").start();
}
public void run() {
int i = 0;
while(true) {
q.put(i++);
}
}
}
class Consumer implements Runnable {
Q q;
Consumer(Q q) {
this.q = q;
new Thread(this, "Consumer").start();
}
public void run() {
while(true) {
q.get();
}
}
}
public class ProducerConsumerDemo {
public static void main(String args[]) {
Q q = new Q();
new Consumer(q); // Consumer started first
new Producer(q); // Producer started second
System.out.println("Press Control-C to stop.");
}
}
Output (synchronous behaviour):
Put: 0
Got: 0
Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5
...
Proper synchronization is demonstrated in this output, where each put()
operation is immediately followed by a get()
operation, guaranteeing data consistency and preventing any values from being missed or overwritten. Strong inter-thread communication requires the valueSet
flag and the while
loops surrounding wait()
.
Important Note on Deprecated Methods
Note that the Thread
class’s suspend()
, resume()
, and stop()
methods are deprecated and should not be used in new Java initiatives. Deadlocks (when a thread is suspended while holding a lock) and data corruption (when a thread is stopped in the middle of updating a data structure) are two major run-time issues that can result from these techniques. Instead, wait()/notify()
are used to arrange suspension and resumption, and threads are designed to check internal flags on a regular basis.