3.1 The Synchronized Keyword
Let's revisit our
AnimatedDisplayCanvas class
from the previous chapter:
package javathreads.examples.ch02.example7;
private volatile boolean done = false;
private int curX = 0;
public class AnimatedCharacterDisplayCanvas extends CharacterDisplayCanvas
implements CharacterListener, Runnable {
...
public synchronized void newCharacter(CharacterEvent ce) {
curX = 0;
tmpChar[0] = (char) ce.character;
repaint( );
}
protected synchronized void paintComponent(Graphics gc) {
Dimension d = getSize( );
gc.clearRect(0, 0, d.width, d.height);
if (tmpChar[0] == 0)
return;
int charWidth = fm.charWidth(tmpChar[0]);
gc.drawChars(tmpChar, 0, 1,
curX++, fontHeight);
}
public void run( ) {
while (!done) {
repaint( );
try {
Thread.sleep(100);
} catch (InterruptedException ie) {
return;
}
}
}
public void setDone(boolean b) {
done = b;
}
}
This example has multiple threads. The most obvious is the one that
we created and which executes the run() method.
That thread is specifically created to wake up every 0.1 seconds to
send a repaint request to the system. To fulfill the repaint request,
the system梐t a later time and in a different thread (the
event-dispatching thread, to be precise)梒alls the
paintComponent() method to adjust and redraw the
canvas. This constant adjustment and redrawing is what is seen as
animation by the user.
There is no race condition between these threads since no data in
this object is shared between them. However, as we mentioned at the
end of the last chapter, other threads invoke methods of this object.
For example, the newCharacter() method is called
from the random character-generating thread (a character source)
whenever the character to be typed changes.
In this case, there is a race condition. The thread that calls
the newCharacter() method is accessing the same
data as the thread that calls the paintComponent(
) method. The random character-generating thread may change
the character while the event-dispatching thread is using it. Both
threads are also changing the X location that specifies where the
character is to be drawn.
A race condition exists because the paintComponent() and newCharacter() methods are not
atomic. It is possible for the
newCharacter() method to change the values of the
tmpChar and curX variables
while the paintComponent() method is using them.
Or for the newCharacter() and
paintComponent() methods to leave the
curX variable in a state that depends on which
individual instructions of the two threads are executed first. We
examine race conditions in more detail later; for now, we just have
to understand that race conditions can generate different results,
including unexpected results, that are dependent on execution order.
|
The term atomic is related to the atom, once considered the smallest
possible unit of matter, unable to be broken into separate parts.
When computer code is considered atomic, it cannot be interrupted
during its execution. This can either be accomplished in hardware or
simulated in software. Generally, atomic instructions are provided in
hardware and are used to implement atomic methods in software.
In our case, we define
atomic
code as code that can't be found in an intermediate
state. In our animated canvas example, if the acts of
"resetting the variable" and
"redrawing one frame of the
animation" were atomic, it would not be possible to
set the variable at the very moment that the character is being
animated. The animation thread also couldn't find
the variables in an intermediate state.
|
The Java specification provides certain mechanisms that deal
specifically with this problem. The Java language provides the
synchronized keyword; in comparison with other
threading systems, this keyword allows the programmer access to a
resource that is very similar to a mutex lock. For our purposes, it
simply prevents two or more threads from calling the methods of the
same object at the same time.
|
A
mutex
lock is also known as a mutually exclusive lock. This type of lock is
provided by many threading systems as a means of synchronization.
Only one thread can grab a mutex at a time: if two threads try to
grab a mutex, only one succeeds. The other thread has to wait until
the first thread releases the lock before it can grab the lock and
continue operation.
In Java, every object has an associated lock. When a method is
declared synchronized, the executing thread must grab the lock
associated with the object before it can continue. Upon completion of
the method, the lock is automatically released.
|
By declaring the newCharacter() and
paintComponent() methods synchronized, we
eliminate the race condition. If one thread wants to call one of
these methods while another thread is already executing one of them,
the second thread must wait: the first thread gets to complete
execution of its method before the second thread can execute its
method. Since only one thread gets to call either method at a time,
only one thread at a time accesses the data.
Under the covers, the concept of synchronization is simple: when a
method is declared synchronized, the thread that wants to execute the
method must acquire a token, which we call a lock. Once the method
has acquired (or checked out or grabbed) this lock, it executes the
method and releases (or returns) the lock. No matter how the method
returns梚ncluding via an exception梩he lock is released.
There is only one lock per object, so if two separate threads try to
call synchronized methods of the same object, only one can execute
the method immediately; the other has to wait until the first thread
releases the lock before it can execute the method.
|