Lesson 3 - Multithreading in Java - Synchronization in practice
In the previous lesson, Multithreading v Javě - Daemon, join, and synchronized, we discussed the topic of synchronization. In today's tutorial, we're going to look at using synchronization in practice.
Context Switching
We've already talked about context switching in the first lesson. This is how the processor switches between individual threads running on a single processor core. And precisely because of this switching, we have to worry about synchronization. As I mentioned, we can never know exactly when the switching will happen. Here's an example:
public class Switcher { public void print0() { while (true) { System.out.print("0"); } } public void print1() { while (true) { System.out.print("1"); } } public void switchIt() { Thread thread = new Thread(this::print0); thread.start(); print1(); } }
We created a Switcher
class with three methods:
The first 2 methods of the class are very simple and simulate some longer work. The first method writes zeros to the console indefinitely, the second method writes ones in the same way.
The switchIt()
method is more interesting to us. It starts
printing ones by itself, but it creates a new thread before assigning the method
printing zeros to it. This thread is then also running.
Edit your main()
method to create the Switcher
object and call the switchIt()
method on it.
Now when we run the program, we probably won't get anything printed. The complete output will not be displayed until the program has finished. Why? Simply because the console doesn't print the incoming characters immediately, but sends them to a buffer, which is printed only after the end of the line. However, sooner or later our binary cocktail will show up. For me it looked like this:
Console application
11111100000000000000000000000000000111111111111111111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000001
11111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000
00000000000000000000000000000000000000111111111111111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
11111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000
00000000000000000000000000000011101000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000001001111111111110000000000000001111111111111111000000001000001111111000000011000000000111
11100000000000001111111111111111111111111111111111111111111111111111111111111100000000000000000000000000
11100100000000000000000000000000000000000000000011111111111111111111111111111111111111111111111111111111
11111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000100000000000
00000000000000000000000000000000000000001111000000000000000000000000000000000000000000000000000000000000
00000000001110000000111111111111111111111111111111111111111111111111111111111111111111111111000011111100
00000000000000000000000000011000000000000000000000000000000001111111111111111111111111111111111111111111
11111111111111110000001111111111111111111111111111111111111100000000000000000000000000000001111111111111
11111111111111111111110001111100000000000001111111111111111111111111111111111111111111111111111111111110
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111
11111111111111111111100001111111111111111111001000001111111111111111111111111111111111111111111111111111
11111111111000000000000000000000000000000000000000000000000000000000000000000011110000111111111110000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111000000000
00001111100000011111100000000000000011001111111111111111111111111111111111111111111111111111111111111111
1111111111111111111110000000000000000000000000000000000000000000000000000
...
Of course, the output will look different every time we run it.
Note that the ones and zeros are not distributed equally, as we might expect. You can see that the thread is running for a while and then it's put to sleep. The intervals also vary, although on average both threads run equally long. I don't think I need to remind you that this is due to context switching. But let's think about how such switching can affect a real application.
We'll program a class representing an ATM machine that contains some cash.
ThreadSafety
A thread can access instance or static variables. This is one of the ways in which threads can communicate with each other. As we already know, the catch will be in the synchronization we've already mentioned. Consider the following class:
class ATMUnsafe { private int cash = 100; private void withdraw100() { if (cash >= 100) { System.out.println("Withdrawing $100"); cash -= 100; System.out.printf("there's still $%d in your account.%n", cash); } } public void withdrawByThreads() { Thread thread1 = new Thread(this::withdraw100); thread1.start(); withdraw100(); if (cash < 0) System.out.println("The cash is negative, we've been robbed."); } }
The class represents an ATM machine that contains some cash. This is $100
when we create the ATM. It also has a simple withdraw100()
method,
which will take $100 if there's a balance needed in the account. The
withdrawByThreads()
method is interesting for us, it tries to
withdraw $100 using 2 threads (the current one and a newly created thread). If
the ATM's cash goes negative, we'll report it.
In the code, I used a reference to the withdraw100()
instance
method using the ::
operator to create a Runnable
object. This is a new syntax since Java 8, and those who don't know what it is
can refer back to the article about
changes made to the new Java version.
We'll add code to the main()
method to make 200 withdrawals from
100 ATMs:
for (int i = 0; i < 100; i++) { ATMUnsafe atm = new ATMUnsafe(); atm.withdrawByThreads(); }
And run the application:
Console application
Withdrawing $100
Withdrawing $100
there's still $0 in your account.
there's still -$100 in your account.
The cash is negative, we've been robbed.
Withdrawing $100
there's still $0 in your account.
Withdrawing $100
there's still $0 in your account.
Withdrawing $100
there's still $0 in your account.
Withdrawing $100
there's still $0 in your account.
Withdrawing $100
Withdrawing $100
there's still $0 in your account.
The cash is negative, we've been robbed.
there's still -$100 in your account.
Withdrawing $100
...
From the output we can see that something is not right. Where's the problem?
In the withdraw100()
method, we check if there's sufficient cash
in the account. Imagine that your account is $100. Thus, the condition is
true
and the system will put the thread to sleep, let's say
immediately after evaluating the condition. So this thread is now waiting. The
second thread also checks the condition that applies and subtracts $100. Then
the first thread wakes up, which has already passed the condition and also
subtracts $100. As a result, we have a negative account balance!
We see that working with threads brings new pitfalls that we haven't yet encountered. To solve this situation, we use synchronization.
Synchronization
Certainly we can agree on the fact that the section verifying the balance and
changing it subsequently must always run uninterrupted, otherwise we come into
the situation above. To solve this problem, we'll wrap this section where we
work with the shared cash
variable with a synchronized
block. We'll edit our ATM class code to:
private int cash = 100; private final Object monitor = new Object(); private void withdraw100() { synchronized (monitor) { if (cash >= 100) { System.out.println("Withdrawing 100"); cash -= 100; System.out.printf("there's still $%d in your account.%n", cash); } } } public void withdrawByThreads() { Thread thread1 = new Thread(this::withdraw100); thread1.start(); withdraw100(); if (cash < 0) { System.out.println("The cash is negative, we've been robbed."); } }
We create a block of code accessible to only one thread at a time using the
synchronized
construct, which takes a monitor as a parameter. The
monitor can be any object, we create a simple field for this purpose. Now, if
the second thread wants to enter the critical (synchronized
)
section, it must wait until the first thread finishes it.
The application now works as it should and can be called thread-safe.
Console application
Withdrawing 100
there's still $0 in your account..
Withdrawing 100
there's still $0 in your account..
Withdrawing 100
there's still $0 in your account..
Withdrawing 100
there's still $0 in your account..
Withdrawing 100
there's still $0 in your account..
Withdrawing 100
there's still $0 in your account..
...
But let's think about whether the code could be optimized. The
synchronized
block wraps all the method code and there's one
monitor for every ATM object. Of course - we can use the
synchronized
keyword in the withdraw100()
method
definition. By default, the ATM object itself will be used as the monitor and
will replace the monitor field.
Let's change the header of the withdraw100()
method to the
following form:
private synchronized void withdraw100()
The program will work the same way. If we remove the monitor field, we save three lines of code and get clearer code as a reward.
Next time, in the lesson Multithreading in Java - Inter-thread Communication, we'll focus on inter-thread communication.