|
|
Synchronization is a means of handling the execution of code between multiple threads running at the same time, inevitably making sections of your code secure so that those sections are not executed concurrently (at the same time) in multiple threads, as this can have disastrous effects on what your program computes.
Now, here's a little reminder on why this section is relevant to you from a games-programming-in-Java point of view. In Java the Event Dispatch Thread runs as a separate process to your main thread of execution. This means that mouse and keyboard event messages, for example, will be posted to event methods for you to then handle appropriately in your game. These methods will be invoked unsynchronized with the main loop, running in the Event Dispatch Thread of execution while the main loop thread continues running on its own. The main loop is basically what controls your game, repeatedly executing, repeatedly handling input, updating the game state, and updating the display of the game each loop cycle. This is similar to our tic-tac-toe game loop in the last chapter but running over and over without pausing for user input each time (we will look at making a main loop from Chapter 9 onward).
Understanding thread synchronization is important because we need to make sure that events coming in from a different thread to our main loop thread are handled in a safe manner, as the threads can lead to executing code that compromises the execution of code in our main loop.
What we are concerned with basically comes down to separate threads sharing the same data and functionality. We can illustrate a very basic example by creating a simple class that handles the getting and setting of a variable. Let's say we had the following simple class:
public class GamePlayer { public int getLives() { return lives; } public void setLives(int l) { lives = l; } private int lives; }
The GamePlayer class simply defines the private instance variable lives and the instance methods getLives and setLives in order to manipulate the lives variable from outside the object. The GamePlayer class has been specially designed in this way for this example.
Now, imagine that two different threads both wanted to execute the following code at relatively the same time:
int lives = gamePlayer.getLives(); gamePlayer.setLives(lives – 1);
As an example, the main loop thread could process this code, but a network message could be read into the network thread also, requiring you to perform the task of decrementing the player's lives value. The expected result in this case should be that the player loses two lives.
If we say that thread A and thread B are both ready to invoke this method, they may execute the code in the following order:
int lives = gamePlayer.getLives(); // thread A gamePlayer.setLives(lives – 1); // thread A int lives = gamePlayer.getLives(); // thread B gamePlayer.setLives(lives – 1); // thread B
Please note that the two local declarations of the lives integer variables in this example have no reference to one another in different threads and should be treated as the separate variables that they are.
With the sequence of execution between the two threads, there are no problems. The lives value of the gamePlayer object is retrieved and then set to the retrieved value minus one. Then the same routine is performed by the second thread. Both of the threads have successfully achieved their goals of decrementing the current value of lives by one. If the lives value started at 7, it would now equal 5. However, that doesn't mean that this code is safe and that this would not necessarily be the outcome. The code in each thread could also be executed in the following sequence:
int lives = gamePlayer.getLives(); // thread A int lives = gamePlayer.getLives(); // thread B gamePlayer.setLives(lives – 1); // thread A gamePlayer.setLives(lives – 1); // thread B
If we trace this path of execution and assume that the value of lives in the gamePlayer object started off at 7, we will get a different outcome. First, both of the threads store the value of the game player's lives into their local lives variables of the assumed value 7. Then in thread A, the new value of lives is set to its current value minus 1, calculated as 7 – 1 and equaling 6. Then thread B will perform exactly the same calculation (7 – 1 = 6). It will not take one off the new value of lives, which is 6, to equal 5 but will take one off the value it originally received, which is 7 and it assumes is still the current value of lives. Thus, two executions of this code from two different threads has resulted in just one life being removed. Why? Because the execution of this code is not synchronized.
The keyword synchronized is the main way in which we can solve such synchronization issues. The keyword synchronized can be used in two distinct ways—to synchronize methods or to synchronize more specific blocks of code. We will begin by looking at synchronized methods.
In order to synchronize a method, you can simply add the keyword synchronized to the method declaration. Now let's suppose that we define the two GamePlayer class methods we saw earlier as follows. Note that this will not completely solve the problem just yet, but let's see:
public class GamePlayer { public synchronized int getLives() { return lives; } public synchronized void setLives(int l) { lives = l; } private int lives; }
The methods getLives and setLives are instance methods of the GamePlayer class and are both declared as synchronized. Now let's suppose that we created an instance of the GamePlayer class as follows.
GamePlayer myPlayer = new GamePlayer();
We will keep this explanation fairly abstract to begin with and leave the discussion of the object's monitor until after.
Let's say we have two threads running, thread A and thread B. If thread A enters one of the methods of the myPlayer object, thread B cannot execute any one of the two synchronized myPlayer methods. When this happens, thread B will pause until thread A releases its hold by exiting the method, whereby thread B can then take its turn.
If, say, one of the two methods is declared as synchronized and the other is not, they are not synchronized with one another. Simply declaring a method alone as being synchronized does not mutually exclude it from other methods of the same object. However, the one synchronized method is synchronized with itself, you might say. A thread executing in this method will prevent any other thread from doing so.
This doesn't solve the problem that two threads could both call getLives first and then both make calls to setLives thereafter, with the error effects discussed earlier. To solve this, we would probably make a method such as the following:
public synchronized void decrementLife() { lives-=1; }
We would probably make the other methods private then for good measure if we needed them at all. Note that all of this seems very messy in general, but the reality is that we can get things synchronized from a higher level at an early stage, as we will see in Chapter 10, "Using the Mouse and Keyboard." So we won't be throwing synchronization statements everywhere like this, but it's essential to understand how things work at the lowest of levels.
Another distinction you need to be aware of is the fact that these methods are only synchronized in the object to which they belong.
Let's say we also declare the following object:
GamePlayer mySecondPlayer = new GamePlayer();
If thread A is running inside one of the synchronized methods of the myPlayer object, thread B cannot access any of the two synchronized methods of the myPlayer object, as we know. However, it is still able to access any of the methods of the mySecondPlayer object, provided another thread isn't currently running in one of those methods, as they are different objects.
What we have just discussed is a very fixed way of looking at synchronization. All we talked about in terms of synchronized methods was that they were instance methods, and any threads would simply invoke one method and then get the heck out of there to allow another thread a look in. If we are to truly understand synchronized methods and thread synchronization as a whole, we must learn about the concept of a monitor.
A monitor can be seen simply as a lock on an object (or a class, as we will touch on in a moment). With this in mind, when a thread wishes to enter a synchronized method of an object, for example, the thread must check to see if any other thread holds that object's monitor. If the object's monitor is owned by another thread, the thread that required the object's monitor must wait until it is released again. When this object's monitor is free, the thread can then take ownership of it. The thread will release the object's monitor when it exits the original method or code block that originally took ownership of the monitor. A thread can also release its ownership of the object's monitor using the wait methods inherited by all objects from the object class, as we will discuss shortly.
As we saw earlier with the GamePlayer object myPlayer, the methods were both synchronized with one another because the monitor, or lock, that they used belonged to the myPlayer object. If thread A was in getLives, thread B could not enter getLives or setLives because thread A owns that object's monitor, to which both methods were synchronized.
As this is the case, you should now be able to see why the synchronized instance methods of different objects (for example, myPlayer and mySecondPlayer) are not synchronized with one another; each object has its own monitor. Adding to this fact, you should realize therefore that a thread can own more than one monitor.
Please do not be confused by the term "monitor"; you can simply see it as a lock, almost a Boolean true or false, or better still a reference to a Thread object that an object or class stores to indicate who has ownership of it (if indeed the object is owned by any thread at all at a given time, that is).
Now, as you might have noticed, in addition to objects having a monitor, a class also has a monitor associated with it. We already know the difference between object methods and class methods. Object methods are instance methods—those that belong to the object—whereas class methods are methods that belong to the class, by declaring them as static. As a class has a monitor itself, we can synchronize static methods of a class by also making them synchronized. We could make the methods of the GamePlayer class static/class methods and synchronize them as follows. Note that this will also entail that we make the lives variable static too.
public class GamePlayer { public static synchronized int getLives() { return lives; } public static synchronized void setLives(int l) { lives = l; } private static int lives; }
If a thread first invokes one of these static methods, it will then have ownership of the class's monitor (not an object monitor), which means that no other threads can enter one of the synchronized static methods of the class until the ownership is released. As objects and classes have their own monitors, synchronization involving different monitors do not affect one another. For example, synchronized static/class methods are not synchronized with synchronized instance methods of that class, as the static methods are synchronized using the monitor of the class and instance methods are synchronized using the monitor of that particular object.
A more powerful use of the synchronization keyword is to specifically synchronize a block of code by specifying an object on which to synchronize. It is very similar to a synchronized instance method of an object in that the thread takes ownership of that object's monitor. With synchronized instance methods, the thread takes ownership of the object's monitor, but with the synchronized block technique, you can specify the object that you wish to synchronize on (it can be any object you want). Furthermore, synchronized methods are obviously restricted by the fact that the whole method is synchronized, and therefore the entire method, if synched with another method of course, is considered a danger area, whereas only one or two lines of the code in the method could actually cause concurrency errors. Moreover, as we can specify any object on which to synchronize, we can mutually exclude the execution of code in different objects (and classes, providing the objects are declared static).
We can synchronize a specific block of code in a method as follows:
public void myMethod { // Code not synchronized here synchronized(myObject) { // Synchronized code here } // Code not synchronized here }
In this example, any threads can enter this method and execute any code. They can then take exclusive access to the synchronized block, taking ownership of the monitor of the object argument of the synchronized block statement, if it's available. Please note that the object parameter myObject could be any object you like; it does not need to be the object you are in, but it could be if you want, where you would pass the argument this to it.
As you can see, with this technique, it is possible for a code block in a method of a different object to synchronize on the same object, mutually excluding code in different objects. Now look at this example:
public class Alpha { public synchronized void myMethod() { } } public class Beta { public void myMethod() { synchronized(myAlphaObject) { // synchronized with myMethod of the Alpha object // myAlphaObject } } }
If we create the Alpha class object myAlphaObject and it is accessible from an object of type Beta, we can synchronize it with the synchronized instance method of myAlphaObject from outside of myAlphaObject.
Note | Synchronizing on a variable reference that is currently equal to null will throw a NullPointerException. |
|
|