Get up to 80 % extra points for free! More info:

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.


 

Previous article
Multithreading v Javě - Daemon, join, and synchronized
All articles in this section
Multithreaded Applications in Java
Skip article
(not recommended)
Multithreading in Java - Inter-thread Communication
Article has been written for you by Matej Kripner
Avatar
User rating:
No one has rated this quite yet, be the first one!
Activities