In this series we'll review several basic concepts to ramp up with concurrent programming in Java. I know, Internet is full of references, tutorials, articles...about the topic, but my intention here is to use a different perspective. Instead of explaining the theory in detail and / or using overly complex examples, I'll try to use a practical approach, developing a simple app from scratch which will grow while introducing different concepts and APIs that we need to know to implement concurrent applications in Java along the way. Hopefully I'll reach my goal :)
The application: Ping Pong
As simple as that. Our Java application will show in the standard output, and alternately the texts "ping" / "pong", along with a header and footer to begin and finish the game:
Game starting...!
ping
pong
ping
pong
//....
Game finished!
There will be two players (or actors), who will print the texts "ping" and "pong". The actor "ping" will play first.
Version Zero: a single thread
The first version will run in a single execution thread, so there won't be any concurrent programming here :). The first versions we'll implement will finish after both players have participated a fixed number of times, let's say 10 (configure in the constant MAX_TURNS
).
This is the code implementing the class Player for our first version:
public class Player {
private final String text;
private int turns = Game.MAX_TURNS;
private Player nextPlayer;
public Player(String text) {
this.text = text;
}
public void play() {
if (!gameFinished()) {
System.out.println(text);
turns--;
nextPlayer.play();
}
}
private boolean gameFinished() {
return turns == 0;
}
public void setNextPlayer(Player nextPlayer) {
this.nextPlayer = nextPlayer;
}
}
Each player prints the text and asks the other player to play, so they're alternating. The class that wires both players and starts the game is quite simple too:
public class Game {
public static final int MAX_TURNS = 10;
public static void main(String[] args) {
Player player1 = new Player("ping");
Player player2 = new Player("pong");
player1.setNextPlayer(player2);
player2.setNextPlayer(player1);
System.out.println("Game starting...!");
player1.play();
System.out.println("Game finished!");
}
}
We can see that the Player
constructor does not have a parameter for the other player. This is because we have a circular reference, so the second player may not be instantiated yet, and we have to inject it through setter.
The attribute text
in Player
is declared as final
. It's a good practice in concurrent applications (in all apps really) to declare class attributes as final
if we know they won't be modified. Not only our code will be more reliable, this guarantees the visibility of our attributes between threads, a concept known as "Safe publication". You can read a good discussion about safe publication here. Going a bit further, we should always try to design our classes as immutable, even though this is not possible in our example.
Version One: Players as Threads
Let's improve our application to make it work concurrently. We'll add one small improvement at a time, discovering that our first approaches don't work as expected.
First, let's make our Player
classes implement Runnable
(more info here):
public class Player implements Runnable {
private final String text;
private int turns = Game.MAX_TURNS;
private Player nextPlayer;
private boolean mustPlay = false;
public Player(String text) {
this.text = text;
}
@Override
public void run() {
while(!gameFinished()) {
while (!mustPlay); //Busy Waiting
System.out.println(text);
turns--;
this.mustPlay = false;
nextPlayer.mustPlay = true;
}
}
private boolean gameFinished() {
return turns == 0;
}
public void setNextPlayer(Player nextPlayer) {
this.nextPlayer = nextPlayer;
}
public void setMustPlay(boolean mustPlay) {
this.mustPlay = mustPlay;
}
}
The important method is run
, it contains a loop that iterates until the number of turns for the player is over. In addition, after every iteration there is a Busy Waiting until it's the turn of the player to act. When this happens the player prints the text, it sets its own mustPlay
to false
and tells the other player to play.
A big difference between this version and the previous one is that, in the first version, one player told the other to play using direct method invocation (i.e. play
), while here it happens modifying the value of a flag (mustPlay
), that each player checks individually and constantly.
Let's see how our class Game
would look like now:
public class Game {
public static final int MAX_TURNS = 10;
public static void main(String[] args) {
Player player1 = new Player("ping");
Player player2 = new Player("pong");
player1.setNextPlayer(player2);
player2.setNextPlayer(player1);
System.out.println("Game starting...!");
player1.setMustPlay(true); //Plays first!!!
Thread thread2 = new Thread(player2);
thread2.start();
Thread thread1 = new Thread(player1);
thread1.start();
System.out.println("Game finished!");
}
}
The biggest difference is that the threads are started separately, and we're only responsible of setting the flag mustPlay
adequately. In fact, I have started the player2's thread first on purpose to confirm that even in this case the first message printed is "ping".
Let's see what happens when we start the application:
Game starting...!
ping
Game finished!
This is not what we expected...what happened? Our application has three threads now:
- Main thread (
Game.main
) - player1's thread
- player2's thread
The problem is that the main thread finishes as soon as the rest of the threads are started, so although the rest of the threads continue their execution and finalize correctly our IDE doesn't reflect the output generated in these two additional threads, creating double confusion because we see the message "Game finished!". To avoid this, there is a simple solution, using the method join
:
public class Game {
public static final int MAX_TURNS = 10;
public static void main(String[] args) {
Player player1 = new Player("ping");
Player player2 = new Player("pong");
player1.setNextPlayer(player2);
player2.setNextPlayer(player1);
System.out.println("Game starting...!");
player1.setMustPlay(true);
Thread thread2 = new Thread(player2);
thread2.start();
Thread thread1 = new Thread(player1);
thread1.start();
//Wait until both threads finish
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Game finished!");
}
}
This method conditions the progress of the thread in execution to the finalization of the thread where it's invoking the method join
. join
is a blocking method (i.e. it blocks the thread which invokes it), and blocking methods can be interrupted, so it throws the ckecked exception InterruptedException
. We'll talk more about the complications to interrupt threads later on this post.
Therefore, in theory our main thread will wait now until both players run out of turns...or maybe not? Well, let's run the app a couple of times, depending on the luck we have it's possible that everything is correct, but if we run it several times it's quite likely that sooner or later we get an output like this:
Game starting...!
ping
Our app is blocked and does not progress at all!
What happened? One of the main problems in concurrent programming is "visibility". Java only guarantees the visibility of attributes between threads if we follow a set of guidelines regulated by the Java Memory Model, concretely by the "happens-before" relationship. According to Wikipedia, in Java this relationship says that:
In Java specifically, a happens-before relationship is a guarantee that memory written to by statement A is visible to statement B, that is, that statement A completes its write before statement B starts its read
In our code we're not following any of the conventions that ensures the visibility of the modification in the attribute mustPlay
between threads. Obviously, the modification of the own player's mustPlay
attribute is visible for the player's thread, who does not continue playing, but, in the way we're doing it, the modification of the attribute mustPlay
of the other thread (nextPlayer.mustPlay = true
) is not automatically visible for the affected thread, so both players have mustPlay
set to false, and our app gets blocked (aka deadlock).
To fix this problem we'll have to introduce the volatile
modifier. This modifier indicates to the JVM that the attribute is likely to be shared between threads, therefore read operations mustn't be cached in any way, accessing to the main memory in all cases. In addition, write operations must be done atomically and made visible immediately.
Our code, then, should be changed this way:
public class Player implements Runnable {
//...
private volatile boolean mustPlay = false;
//....
}
And now, finally, the app works in a deterministic way in every execution. One of the main problems of thread visibility in concurrent applications is that it fails randomly, so, if we're not aware about the guidelines to follow, debugging these problems can be extremely complicated.
Version 2: Infinite game
Instead of playing a fixed number of turns we're going to make both players to play forever. Or better said, until the main thread wants. To achieve this, we'll have to use one of the features that Java offers to interrupt a thread. Let's see how our Game
class would look like:
public class Game {
public static void main(String[] args) {
Player player1 = new Player("ping");
Player player2 = new Player("pong");
player1.setNextPlayer(player2);
player2.setNextPlayer(player1);
System.out.println("Game starting...!");
player1.setMustPlay(true);
Thread thread2 = new Thread(player2);
thread2.start();
Thread thread1 = new Thread(player1);
thread1.start();
//Let the players play!
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//Tell the players to stop
thread1.interrupt();
thread2.interrupt();
//Wait until players finish
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Game finished!");
}
}
We see that, once both player threads are started, the main class goes to sleep for a while (2ms), and as soon as it wakes up (it returns to "running" state actually), it requests both player threads to finish.
I repeat, it requests. The only thing that happens when the method interrupt
is invoked on a thread is that a flag interrupted
is set to true in that thread. It's responsibility of the own thread to act when it considers necessary, perform cleaning tasks, and finalize. But it's perfectly valid if the thread decides not to do anything and continue with its execution (even though that wouldn't be very correct, of course). The way to know the value for this flag is using the method Thread.interrupted()
, so our class Player
would change to:
public class Player implements Runnable {
private final String text;
private Player nextPlayer;
private volatile boolean mustPlay = false;
public Player(String text) {
this.text = text;
}
@Override
public void run() {
while(!Thread.interrupted()) { //Was I interrupted?
while (!mustPlay);
System.out.println(text);
this.mustPlay = false;
nextPlayer.mustPlay = true;
}
}
public void setNextPlayer(Player nextPlayer) {
this.nextPlayer = nextPlayer;
}
public void setMustPlay(boolean mustPlay) {
this.mustPlay = mustPlay;
}
}
Instead of checking after every iteration if we have run out of turns we check the state of the flag interrupted
, and we finish if it's true. As simple as that.
Version 2b: More about interrupt
Before finishing this first post of the series, let's delve into the implications of interrupting a thread.
In some occasions, we've seen that some of the methods in the class Thread
(join
, sleep
,...) throw InterruptedException
. This happens when a thread is interrupted being in blocked state after invoking any of these methods (e.g. anotherThread.join()
). In that case, what happens is that the method (join
, to follow with the example), sets the flag interrupted
to false in the thread and throws InterruptedException
. I'm not a big fan of checked exceptions, but I think this is one of the few cases where their use is more than justified.
Let's modify the class Player
slightly, so once it's its turn to play it goes to sleep for 1ms before printing the text:
public class Player implements Runnable {
//...
@Override
public void run() {
while(!Thread.interrupted()) {
while (!mustPlay);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace(); //1
}
System.out.println(text);
this.mustPlay = false;
nextPlayer.mustPlay = true;
}
}
//...
}
If we keep using the same version of the Game
class, it's very likely that the game lasts forever. Why? Because if the interruption happens when the player thread is sleeping, the method sleep
will swallow the "interrupted" state before throwing the exception, and given that we're only printing the error in //1, the loop does not detect the state interrupted and continues forever.
The solution to this problem is reestablishing the Thread state to interrupted:
@Override
public void run() {
while(!Thread.interrupted()) {
while (!mustPlay);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
//I was interrupted, propagate the state
Thread.currentThread().interrupt();
}
System.out.println(text);
this.mustPlay = false;
nextPlayer.mustPlay = true;
}
}
In general, we have to be very careful when we handle InterruptedException
. Another recommended strategy, which would imply modifying the logic in our run
method, is rethrowing the exception, so it will be handled somewhere else. We should never swallow the exception.
There are many improvements to carry out, the application is far from being optimal (that horrible Busy Waiting...). In the next post we'll optimize the use of the CPU using locks and conditions.
(All the code can be found in this GitHub repository)
Top comments (0)