Let's imagine, we are writing a program that reads events from the network. Then performs computations based on what's been received. Possibly, a simplified Event could look like that.
public class Event {
private final String name;
private final String content;
Event(String name, String content) {
this.name = name;
this.content = content;
}
public String getName() {
return name;
}
public String getContent() {
return content;
}
}
Now, we can create an entity that will do all the magic, read data from the socket and provide us Events. Let's create a stub for that and name it NetworkQueue.
public class NetworkQueue {
public Event poll() {
return new Event(
UUID.randomUUID().toString(),
UUID.randomUUID().toString());
}
}
We would like also to track the number of received Evens for processing. For that, we will introduce Counter.
public class Counter {
private int cnt = 0;
public void increment() {
++cnt;
}
public int getCnt() {
return cnt;
}
}
We need also a place where real processing will occur. So let's create an EventProcessor that will print the Event's content on standard output for now.
public class EventProcessor {
public void handle(Event e) {
System.out.println(e.getContent());
}
}
Now, we can put all the pieces together and receive 10000 Events.
public class Main {
private static final NetworkQueue queue = new NetworkQueue();
private static final Counter counter = new Counter();
private static final EventProcessor processor = new EventProcessor();
public static void main(String[] args) {
int i = 10000;
while (i > 0) {
Event event = queue.poll();
counter.increment();
processor.handle(event);
i--;
}
System.out.println("Processed events: " + counter.getCnt());
}
}
So, we have the main loop where we pick up an event from the queue, increment the counter, and then pass the event to the processor. When we run the program, we will get the following output.
...
f53fe4f3-d2ab-4fa3-9f6f-82124c25c9ae
e0850b6a-0858-4a6e-bb9d-35dc066dc400
82f883e3-020b-470c-8986-da4b5a18b24b
Processed events: 10000
That's a good start! 10000 Events processed!
However, processing those events one by one, sequentially, will take a lot of time and will be really slow. Usually, we would like to speed things up and process more events simultaneously. As a matter of fact, nowadays CPUs have more than a single core, so why not just make use of them? Let's assume we have a CPU with 4 cores and would like to make use of all of them, so small adjustments are necessary.
public class Main {
private static final NetworkQueue queue = new NetworkQueue();
private static final Counter counter = new Counter();
private static final EventProcessor processor = new EventProcessor();
private static final int noOfThreads = 4;
private static final ExecutorService executor = Executors.newFixedThreadPool(noOfThreads);
public static void main(String[] args) throws ExecutionException, InterruptedException {
int i = 10000;
Runnable runnable = () -> {
Event event = queue.poll();
counter.increment();
processor.handle(event);
};
Collection<Future<?>> futures = new ArrayList<>();
while (i > 0) {
futures.add(executor.submit(runnable));
i--;
}
for (var future : futures) {
future.get();
}
executor.shutdown();
System.out.println("Processed events: " + counter.getCnt());
}
}
We have created a fixed thread pool and set it to 4. We have also extracted the "main part" to Runnable. Then, it is submitted to the executor 10000 times to process 10000 Events. We save futures to be able to wait until all of them finishes. Of course, once processing is done, we shut the executor down.
Now, we run it! Once it finishes, we can observe the following discrepancy.
...
8f5df64e-b6b7-4694-a5a9-1ef335cd55c4
0ec0514b-f523-47ab-a017-221396f5fc9a
Processed events: 9997
We have submitted 10000 Events however, our Counter counted 9997 Events only.
synchronized
When we were processing Events in a single thread, Counter showed proper values. Once we scaled threads up, Counter started to show improper values. The issue lies in how memory is structured. Fetching data from a hard drive is ultra-slow. Fetching data from RAM is slow. To make computations super fast, the processor is equipped with registers and caches. Manipulation of data may take place in registers however, data itself is prefetched from the L1 cache, not directly from RAM. And most modern processors have an L1 cache per core. When we have multiple threads of execution, each thread might be scheduled on a different core, therefore each thread might have its "own" example of cnt from Counter. That's why there is a need for a synchronization mechanism, so cnt can be updated by one thread at a time and updated value can be pushed to other threads.
JAVA contains an embedded synchronization mechanism that comes with synchronized keyword. Once we decorate the method with the keyword:
only a single thread can execute the method,
other threads have to wait until the first thread finished execution,
values of modified variables are synchronized between threads.
Let's try to fix the Counter.
public class Counter {
private int cnt = 0;
public synchronized void increment() {
++cnt;
}
public synchronized int getCnt() {
return cnt;
}
}
Now, when running the program, we get following output.
...
8dda53a3-4abf-400e-9fb8-812fa5296ef5
33234d8a-b9a7-48ab-8017-07d1c069894c
3f3a13ad-4470-4acb-b127-28bf92e9fc37
Processed events: 10000
We've made it! The counter now shows the correct value.
static synchronized
Let's imagine we do have this Conter in our code, but someone has come to the conclusion that it doesn't make sense to instantiate the Counter class as we use a single instance only. The idea was to convert it into singleton, but still, leave the possibility to instantiate class as we don't want to refactor all the code at once.
public class Counter {
private static int cnt = 0;
public synchronized void increment() {
++cnt;
}
public static synchronized int getCnt() {
return cnt;
}
public static synchronized void inc() {
++cnt;
}
}
Counter is slightly modified. cnt is decorated with static. A new synchronized static inc() method is added. Still, we leave the possibility to instantiate Counter.
Now, let's modify the main loop.
...
Runnable runnable = () -> {
Event event = queue.poll();
counter.increment();
processor.handle(event);
};
Runnable runnableStatic = () -> {
Event event = queue.poll();
Counter.inc();
processor.handle(event);
};
Collection<Future<?>> futures = new ArrayList<>();
while (i > 0) {
Runnable r = (i % 2 == 0) ? runnable : runnableStatic;
futures.add(executor.submit(r));
i--;
}
...
We have an old "legacy" runnable that uses instances of the counter. We also have a new runnableStatic that uses the static inc() method. While loop is modified to pick every second time runnableStatic. Now, when we run it, we get the following output.
...
6a0bcc4d-343b-4cc3-bfd5-8746b81234f9
236cab24-a395-4e26-be56-9923b6567f2a
131fa8df-fffa-4fa1-bdfe-fbad5bfdc5eb
Processed events: 9998
Counter is corrupted again.
It is worth stopping for a moment and finding out how synchronization works underneath. JAVA internally uses monitors to provide synchronization. And those monitors are bound to an object. Instance methods are synchronized over the instance of the class that owns the method, so the monitor is bound to this. And that means only one thread per instance of the class can execute the method. In contrast, static methods are synchronized on the Class object associated with the class. Since only one Class object exists per JVM per class, only one thread can execute inside a static synchronized method per class.
To fix Counter, we have to refactor the rest of the code and get rid of the instance method.
public class Counter {
private static int cnt = 0;
private Counter() {}
public static synchronized int getCnt() {
return cnt;
}
public static synchronized void inc() {
++cnt;
}
}
Now, we rework the main loop to use static methods.
...
Runnable runnable = () -> {
Event event = queue.poll();
Counter.inc();
processor.handle(event);
};
Collection<Future<?>> futures = new ArrayList<>();
while (i > 0) {
futures.add(executor.submit(runnable));
i--;
}
...
It is worth noticing that we call inc() method on Counter class directly instead of calling it on that class' instance. When we run the program, everything gets back to normal.
0e288129-e4df-461b-a1a9-9e64d100aea7
a6d30ddd-6905-45a2-bf5b-e7ab9aea15e7
fa9c2561-59fa-4c5e-bd17-07378d6360ea
Processed events: 10000
Scope of synchronization
When decorating methods with synchronized keyword, a synchronization mechanism is applied to the entire method. However, sometimes we want to narrow the scope of synchronization and apply it to a part of the method. We can achieve that using synchronization blocks. When using them, we always have to provide explicitly a monitor object.
Let's see Counter "instance" example and its equivalence using synchronization blocks.
public class Counter {
private int cnt = 0;
public synchronized void increment() {
++cnt;
}
public synchronized int getCnt() {
return cnt;
}
}
And equivalent code.
public class Counter {
private int cnt = 0;
public void increment() {
synchronized (this) {
++cnt;
}
}
public int getCnt() {
synchronized (this) {
return cnt;
}
}
}
Now, let's see static Counter implementation once again.
public class Counter {
private static int cnt = 0;
private Counter() {}
public static synchronized int getCnt() {
return cnt;
}
public static synchronized void inc() {
++cnt;
}
}
The equivalent code with synchronization block is as follows.
public class Counter {
private static int cnt = 0;
private Counter() {}
public static int getCnt() {
synchronized (Counter.class) {
return cnt;
}
}
public static void inc() {
synchronized (Counter.class) {
++cnt;
}
}
}
With {} brackets, we can narrow the synchronization scope down to the instructions that are critical for us. For instance, we can call other methods before executing the critical section.
public void increment() {
printSomething();
synchronized (this) {
++cnt;
}
}
Conclusion
In this article, we briefly discussed what is the purpose of synchronized keyword in JAVA, and how it can prevent race conditions in the program. We also described how static keyword impacts synchronized keyword as well as learned about synchronization scopes.