Table of Contents
 1. Problem 1 â Main thread doesn't wait for other threads to finish
 2. Solution â future.get()
 3. Problem 2 â Multiple threads have access to the same variable at a time
 4. Solution â AtomicInteger
 5. Problem 3 â AtomicInteger
may be not enough
 6. Solution â synchronized
keyword
      Synched non-static method
      Another synched non-static method
      Simple unsynched method
      Static synched method
      Sounds overwhelming. How to understand it?
 7. AtomicInteger vs synchronized
 8. Conclusion
 9. Useful resources
Hello, everyone! First of all, thank you all for the amazing feedback and support you gave me on my previous post. Thanks @thepracticaldev for supporting aspiring writers. I'm so happy to be back with another article about how to synchronize threads!
Well, why should we synchronize them if the essence of multithreading is to perform operations asynchronously? Because with the benefits of multithreading come challenges. In this article, weâll learn about problems that occur and ways to solve them.
Letâs consider this code:
public class DonutStorage {
private int donutsNumber;
public DonutStorage(int donutsNumber) {this.donutsNumber = donutsNumber;}
public int getDonutsNumber() {return donutsNumber;}
public void setDonutsNumber(int donutsNumber) {this.donutsNumber = donutsNumber;}
}
public class Consumer {
private final DonutStorage donutStorage;
public Consumer(DonutStorage donutStorage) {this.donutStorage = donutStorage;}
/**
* Subtracts the given number from the DonutStorage's donutsNumber.
* @param numberOfItemsToConsume Number that will be subtracted from the donutsNumber
*/
public void consume(int numberOfItemsToConsume) {
donutStorage.setDonutsNumber(donutStorage.getDonutsNumber() - numberOfItemsToConsume);
}
}
public class Main {
public static void main(String[] args) {
int consumersNumber = 10;
DonutStorage donutStorage = new DonutStorage(20);
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
for (int i = 0; i < consumersNumber; i++) {
executor.submit(() -> new Consumer(donutStorage).consume(1));
}
executor.shutdown();
System.out.println("Number of remaining donuts: " + donutStorage.getDonutsNumber());
}
}
Here we have a simple program for a donut shop that counts the number of donuts. This shop probably has a server, which gets data from clients. As there are many clients who consume donuts, and the server must serve them simultaneously, it will have different threads each dedicated to one user.
To realize this in code, we create an ExecutorService
with the number of threads equal to the available number of cores. Then we want it to serve the consumerNumber
of clients, so there is a for loop that submits tasks to create a new Consumer
that will consume items from the DonutStorage
, and print the number of remaining donuts on the console.
Letâs assume that there are 10 consumers, each buys 1 donut, and we have 20 donuts in total. To prevent errors, if a user wants more donuts than there are in stock, the remaining donuts will be sold (the donutsNumber will be set to 0).
Problem 1
So we expect the number of remaining donuts to be 10, but I have the following printed (you may have a different result):
Number of remaining donuts: 19
Question
Why is that so? This problem is not connected with race condition. So go ahead and make a guess! Câmon, you should know the answer from the previous article!
Answer
Because we didnât call join
so the main thread doesnât wait for the others to finish.
Donât get confused by executor.shutdown()
. It doesnât mean that the code below it is executed after the executor is shut down. Look, the main thread does 5 things:
- creates some variables;
- creates an executor;
- submits tasks to the executor;
- shuts down the executor;
- prints the result;
These operations are done sequentially, but, when it tells the executor to shut down, it doesnât actually wait until it happens. According to the documentation of shutdown()
, when this method is called, ExecutorService
Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.
However, the main thread isnât blocked and continues its work.
In other words, in my situation, only 1 of the 10 users had managed to get a donut, when the main thread printed the number of remaining ones. As threads can be scheduled in other ways, rerunning the program, we may get a different result.
Solution
But how can we join the threads? With a simple Thread
we would write myThread.join()
. But how to do it with ExecutorService
?
Itâs not appropriate to use ExecutorService
in this way. We should harness its Future
power instead.
Even though, executor.submit(()->{})
submits a Runnable
, the method still returns a Future
that never contains any value inside but can tell when the task gets finished.
To get the desired functionality, we should keep these futures from each submission in a list and then loop through them to make sure all the tasks are finished:
public class Main {
public static void main(String[] args) {
int consumersNumber = 10;
DonutStorage donutStorage = new DonutStorage(20);
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
List<Future<?>> futures = new ArrayList<>(consumersNumber);
for (int i = 0; i < consumersNumber; i++) {
futures.add(executor.submit(() -> new Consumer(donutStorage).consume(1)));
}
executor.shutdown();
// make the main thread wait for others to finish
for (Future<?> future: futures) {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
System.out.println("Exception while getting from future" + e.getMessage());
e.printStackTrace();
}
}
System.out.println("Number of remaining donuts: " + donutStorage.getDonutsNumber());
}
}
This way the main thread gets blocked (waits) until the other threads finish their work.
BTW, itâs wrong to assume that getting only the final future is enough because the tasks arenât done sequentially.
You can compare the old and the updated versions here.
Problem 2 â Multiple threads have access to the same variable at a time
Change the main method and run the code. Output:
Number of remaining donuts: 10
The problem is solved, right? Unfortunately, no. Try and run it again! Output:
Number of remaining donuts: 12
Explanation
Let's examine what may have happened.
You see, multiple threads have access to the private int donutsNumber
in the DonutStorage
at the same time. To change the value of this variable they should:
- Get the variable
- Set the variable
Because of the thread interleaving we may get something like this:
The 5th thread reads the value of 15 | |
The 6th thread reads the value of 15 (as the 5th user hasnât yet changed the value) | |
The 5th thread changes the value to 14 | |
The 6th thread changes the value to 14 |
Blank in this table means that the thread for whatever reason sits idle, which is completely normal.
So everything goes OK until the 5th and the 6th threads read the same value simultaneously and one item is automatically not counted. And it may not be the only case. This behavior is called a race condition.
Solution â AtomicInteger
To solve a synchronization problem we first should check if the solution is already written for us. In this case, there is one - AtomicInteger
.
An atomic operation is an operation that is done at once. As weâve seen, subtracting a number from a variable is not an atomic operation. Similarly, when you write i++
, it seems to be atomic, but itâs not because the value is read and then written (so two operations not one).
There are classes like AtomicInteger
, AtomicLong
, AtomicBoolean
, etc. They internally make these operations atomic and in this way solve the problem.
Here is the improved code:
public class DonutStorage {
private final AtomicInteger donutsNumber;
public DonutStorage(int donutsNumber) {
this.donutsNumber = new AtomicInteger(donutsNumber);
}
public AtomicInteger getDonutsNumber() {return donutsNumber;}
}
public class Consumer {
private final DonutStorage donutStorage;
public Consumer(DonutStorage donutStorage) {
this.donutStorage = donutStorage;
}
/**
* Subtracts the given number from the DonutStorage's donutsNumber.
* @param numberOfItemsToConsume Number that will be subtracted from the donutsNumber
*/
public void consume(int numberOfItemsToConsume) {
donutStorage.getDonutsNumber().addAndGet(-numberOfItemsToConsume);
}
}
Firstly, we change the donutsNumber
âs type to AtomicInteger
, modify the respective getter, and delete the setter because we no longer need it. Then, the Consumer
âs consume
method should be modified to work with AtomicInteger
. Letâs use its addAndGet
method, which atomically adds the given number to the current value or subtracts from it if the given param is negative.
Mind you, it wouldnât work if weâd modified getters and setters for donutsNumber
to return int
instead of making the consume
method work with AtomicInteger
. Thatâs because different threads wouldâve ended up using int
and bypassing the atomic nature of AtomicInteger
. So be careful with this and make sure that you are using it correctly. However, this problem will be explained in more detail in the following section.
You can compare the old and the updated versions here.
Problem 3 â AtomicInteger
may be not enough
Now consumers are allowed to take as many items as they want, but the number of donuts is limited. Letâs improve the program to satisfy this requirement.
So weâll add an if-statement to the consume
method that checks if the given number is bigger than the number of donuts in stock. When so, the number of donuts in stock will be set to 0. But itâll be also good to know how many donuts a consumer actually consumes so letâs make the consume
method return this number. Weâll also change the lambda in the Main
to print the results in the form of âThreadâs name
consumed number of items
â. To test how it works, letâs also make the consumers try to consume more than is available, say 3 items each. The changes may look like this:
futures.add(executor.submit(() -> {
Consumer consumer = new Consumer(donutStorage);
System.out.println(Thread.currentThread().getName() + " consumed " +
consumer.consume(3)); // changed the number from 1 to 3
}));
/**
* Subtracts the given number from the DonutStorage's donutsNumber. If the given number is bigger
* than the number of donuts in stock, sets the donutsNumber to 0.
* @param numberOfItemsToConsume Number that will be subtracted from the donutsNumber
* @return the number of consumed items
*/
public int consume(int numberOfItemsToConsume) {
AtomicInteger donutsNumber = donutStorage.getDonutsNumber();
// if there aren't enough donuts in stock, consume as many as there are
if (numberOfItemsToConsume > donutsNumber.get()) {
int result = donutsNumber.get();
donutsNumber.set(0);
return result;
}
donutStorage.getDonutsNumber().addAndGet(-numberOfItemsToConsume);
return numberOfItemsToConsume;
}
You can compare the old and the updated versions here.
Running the code, we can get the desired result!
pool-1-thread-1 consumed 3
pool-1-thread-3 consumed 3
pool-1-thread-6 consumed 3
pool-1-thread-8 consumed 0
pool-1-thread-7 consumed 2
pool-1-thread-1 consumed 0
pool-1-thread-2 consumed 3
pool-1-thread-5 consumed 3
pool-1-thread-4 consumed 3
pool-1-thread-7 consumed 0
Number of remaining donuts: 0
You see how threads interleave. The one which gets only 2 items doesnât go after the others that get 3 items. Itâs completely OK. Whatâs not OK is that if we add a simple action that takes some time into the consume
method, for example, printing some random text it will break the program. Check this out:
public int consume(int numberOfItemsToConsume) {
AtomicInteger donutsNumber = donutStorage.getDonutsNumber();
// if there aren't enough donuts in stock, consume as many as there are
if (numberOfItemsToConsume > donutsNumber.get()) {
int result = donutsNumber.get();
donutsNumber.set(0);
return result;
}
// printing some random text breaks the program
System.out.println("The UFO flew in and left this inscription here.");
donutStorage.getDonutsNumber().addAndGet(-numberOfItemsToConsume);
return numberOfItemsToConsume;
}
Output:
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
The UFO flew in and left this inscription here.
pool-1-thread-4 consumed 3
pool-1-thread-4 consumed -4
pool-1-thread-4 consumed 0
pool-1-thread-6 consumed 3
pool-1-thread-5 consumed 3
pool-1-thread-7 consumed 3
pool-1-thread-3 consumed 3
pool-1-thread-2 consumed 3
pool-1-thread-1 consumed 3
pool-1-thread-8 consumed 3
Number of remaining donuts: 0
Despite the resulting number still being 0, we have issues with the resulting value of consume
method. Why is that so?
Thatâs because printing some text introduces a delay resulting in the interleaving of the threads in a way that causes another race condition. Add it doesnât mean that we shouldnât just write some random text and the program will work fine. On another personâs computer, it may return an unexpected output without the random text, because their computer may be slower/faster/less or more busy, etc.
The question is why the program breaks despite AtomicInteger
. Another question is why the race condition doesnât happen in the previous sectionâs code. And you are about to get the answers.
Explanation
Thatâs because AtomicInteger
only provides atomic access to the value it holds and uses a non-blocking algorithm. And that means that multiple threads may access the value at the same time. Whereas, our program contains consume
method that gets executed by multiple threads simultaneously.
Here is a possible scenario:
The 6th thread reads the value of 5 and gets false in the if-condition | |
The 7th thread reads the value of 5 and gets false in the if-condition either | |
The 6th thread atomically reads 5 and writes 2 | |
The 7th thread atomically reads 2 and writes -1 |
You see that the 7th user may make the value negative because it has already passed the check and moved from the if-statement when it updates the value.
Solution â synchronized
keyword
Luckily, the solution to this is quite simple â just use the synchronized
keyword for the critical section. In our case, itâs the whole consume
method. This will tell the JVM that only one thread at a time can execute the critical section.
Here is some info you need to know regarding this:
In Java, this keyword can be used with (both static and non-static) methods and code blocks:
public synchronized void m1(){}
public static synchronized void m2(){}
synchronized (this) {}
synchronized (MyClass.class) {}
Synched non-static method
This means that when there is an Object, for instance, donutStorage
that has the following method:
public synchronized void setDonutsNumber(int donutsNumber) {
this.donutsNumber = donutsNumber;
}
only 1 thread will be allowed to execute it (set the variable) at a time.
Another synched non-static method
And when there is another synchronized method in this class:
public synchronized int getDonutsNumber() {
return donutsNumber;
}
another thread will not be allowed to execute it, while the first one is executing setDonutsNumber()
, even though this method does not depend on setDonutsNumber()
However, if another synchronized method is called within a synchronized method or code block it will not cause any issues and the thread executing it will freely run that method.
// the thread that is executing m1 will freely get access to m2
// and execute it, even though m2 is synchronized
public synchronized void m1() {
m2();
}
public synchronized void m2() {}
Simple unsynched method
However, if there is an unsynchronized method in this class, for example:
public void optimizeDatabase() {}
multiple other threads will be allowed to execute it
Static synched method
In addition, if there is a static synchronized method in this class, for example:
public static synchronized double calculateAverageDonutWeight(Donut[] donuts) {}
another thread will be allowed to execute it, even though the first one is executing setDonutsNumber
because one of them is static and another one is non-static.
Similarly to non-static, if there is another static synchronized method*,* other threads will be restricted from executing it, while this one is being executed.
Sounds overwhelming. How to understand it?
Sounds overwhelming, but in fact, this is pretty easy to understand.
A synchronized method tells the thread executing it to acquire a lock, which doesnât allow any other threads to pass if they donât have the lock.
By default, there are only two locks associated with a class: class-level lock (for static methods), and object-level lock (for non-static methods).
Synchronized blocks require an explicit indication of what lock they use.
synchronized (this) {} // object-level lock
synchronized (MyClass.class) {} // class-level lock
There is a good visual explanation in this video. It is also a good idea to play around with code to understand the topic and put the necessary info into your long-term memory.
Updated code
Mind you, in this case, when we use synchronized
, AtomicInteger
is no longer needed. Thatâs because itâs only consume
method that causes the race condition and no one else. Keeping AtomicInteger
will only cause performance degradation. However, if you eventually decide to get multithreaded access to the DonutStorage
in other parts of your code, you will need to use synchronized
either (or find another solution).
Hereâs the updated code:
// DonutStorage is the same as it was in the beginning
public class DonutStorage {
private int donutsNumber;
public DonutStorage(int donutsNumber) {this.donutsNumber = donutsNumber;}
public int getDonutsNumber() {return donutsNumber;}
public void setDonutsNumber(int donutsNumber) {this.donutsNumber = donutsNumber;}
}
public int consume(int numberOfItemsToConsume) {
synchronized (donutStorage) {
int donutsNumber = donutStorage.getDonutsNumber();
// if there aren't enough donuts in stock, consume as many as there are
if (numberOfItemsToConsume > donutsNumber) {
donutStorage.setDonutsNumber(0);
return donutsNumber;
}
donutStorage.setDonutsNumber(donutsNumber - numberOfItemsToConsume);
return numberOfItemsToConsume;
}
}
You can compare the old and the updated versions here.
Also, note that making the consume
method synchronized
wouldnât work. Thatâs because a thread entering it would have to acquire a lock for the Consumer
object, while we want it to get a lock for the donutStorage
.
There is no point in getting a lock for the Consumer
object because each thread has its own Consumer
object therefore each Consumer
is used by only one thread. On the other hand, donutStorage
is a single instance of the DonutStorage
class throughout our code. Getting a lock for it before executing the consume
method forces other threads to wait until the current one releases that lock.
Output:
pool-1-thread-3 consumed 0
pool-1-thread-7 consumed 3
pool-1-thread-5 consumed 3
pool-1-thread-8 consumed 3
pool-1-thread-1 consumed 3
pool-1-thread-2 consumed 3
pool-1-thread-4 consumed 2
pool-1-thread-6 consumed 3
pool-1-thread-8 consumed 0
pool-1-thread-3 consumed 0
Number of remaining donuts: 0
You can also add that print-random-text command to make sure that it works properly.
AtomicInteger
vs synchronized
Now you may be wondering why he told me about AtomicInteger
if itâs easier just to use synchronized
.
Well, atomic classes are faster as they donât use blocking. Not using blocking is also better for preventing deadlocks, which will be brought up in the following article. Also, when you use an atomic class and want to create another thread-safe block of code, you will not need to synch it.
However, as you saw, atomic classes donât fully prevent race conditions. So letâs break it down to understand when to use AtomicInteger
over synchronized
.
AtomicInteger
variable is an equivalent of a simple variable with synchronized getters and setters:
This:
public class DonutStorage {
private final AtomicInteger donutsNumber;
public DonutStorage(int donutsNumber) {
this.donutsNumber = new AtomicInteger(donutsNumber);
}
public AtomicInteger getDonutsNumber() {return donutsNumber;}
}
Will give the same result as this:
public class DonutStorage {
private int donutsNumber;
public DonutStorage(int donutsNumber) {this.donutsNumber = donutsNumber;}
public synchronized int getDonutsNumber() {return donutsNumber;}
public synchronized void setDonutsNumber(int donutsNumber) {this.donutsNumber = donutsNumber;}
}
The code in the âAtomicInteger
may be not enoughâ section doesnât work, whereas the code in âSolution â AtomicInteger
" works fine. Thatâs because the latterâs consumer
method contains only 1 operation (atomic subtraction from the value) whereas the formerâs contains as many as 4 accesses to the value.
So, when you need to perform simple updates on the variable,
AtomicInteger
is best. But, if your synchronization scope is bigger than that usesynchronized
.
In addition, I wanted to emphasize that itâs recommended to first search for some ready and well-tested solutions for your problem.
There is also a good explanation of this on StackOverflow.
Conclusion
Sorry for loading you with that amount of information. Unfortunately, multithreading is not an easy topic. You could just learn to simply use synchronized
and thatâs it, but the real problems would come in practice. I tried to provide real examples, problems, solutions to them, and common gotchas.
To sum up, race conditions are common multithreading problems that sometimes may be not easy to spot. Itâs important to synchronize threads if they have access to the shared resource. Of course, atomic classes and synchronized
arenât the only ways to synchronize threads. The solution to your specific problem depends on the complexity of your code and what actually you want to get. Itâs always better to first search for a ready solution.
Thank you for reading this article! Iâm excited to write a new one in a few days. Itâs going to be about inter-thread communication, particularly explaining wait
and notify
methods, and probably SynchronousQueue
. If you learned something from this tutorial, please provide some feedback. If you have any questions or corrections, I would love to hear from you in the comments.
Useful resources
Here are some useful resources that I learned from.
- The code from this article is available on GitHub.
- Synchronization | GeeksforGeeks (YouTube video) â a good visual representation of locks.
-
Synchronization while using AtomicInteger | Stack Overflow â if you didnât understand when to use
AtomicInteger
oversynchronized
. -
Synchronized Block for .class | Stack Overflow â if you didnât understand what is
synchronized (MyClass.class)
Top comments (0)