Java Reference
In-Depth Information
The sequence of events is as follows:
•
thread1
starts first and synchronizes on
theObject
. This prevents any methods for
theObject
being called by any other thread.
•
thread1
then calls
sleep()
so
thread2
can start.
•
thread2
starts and synchronizes on
theOtherObject
. This prevents any methods for
theOther-
Object
being called by any other thread.
•
thread2
then calls
sleep()
, allowing
thread1
another go.
•
thread1
wakes up and tries to call
method2()
for
theOtherObject
, but it can't until the code
block in
thread2
that is synchronized on
theOtherObject
completes execution.
•
thread2
gets another go because
thread1
can't proceed and tries to call
method1()
for
theOb-
ject
. This can't proceed until the code block in
thread1
that is synchronized on
theObject
com-
pletes execution.
Neither thread has any possibility of continuing — they are deadlocked. Finding and fixing this sort of
problem can be very difficult, particularly if your program is complicated and has other threads that continue
to execute.
You can create a trivial deadlock in the last example by making the
for
loop in
main()
synchronized on
one of the accounts. For example:
synchronized(accounts[1]) {
for(int i = 1 ; i <= transactionCount ; ++i) {
// code for generating transactions etc...
}
}
A deadlock occurs as soon as a transaction for
accounts[1]
arises because the
doTransaction()
meth-
od in the
theBank
object that is called by a
Clerk
object to handle the transaction is synchronized to the
same object and can't execute until the loop ends. Of course, the loop can't continue until the method in the
theBank
object terminates, so the program hangs.
In general, ensuring that your program has no potential deadlocks is extremely difficult. If you intend to
do a significant amount of programming using threads, you need to study the subject in much more depth
than I can deal with here.
USING EXECUTORS
An
executor
is an object that you can use to start and manage threads. This can make thread programming
much easier and more efficient. An executor can execute a
Runnable
task, which is the kind of task that is
already familiar to you. It can also execute a
Callable<V>
task, which is a task that can execute in a separate
thread, just like a
Runnable
task, but which also returns a value on completion. The type parameter,
V
, for
the interface is the type of the value to be returned by the class, so a
Callable<>
class type must implement
the interface with the appropriate type specified.
The
Callable<>
interface specifies just one method,
call()
, that a
Callable<>
object must implement.
The
call()
method is the equivalent of the
run()
method in the
Runnable
interface, but in addition returns
a value when the method terminates. Thus you can define a task that you want to execute as a separate thread
either by a class that implements the
Runnable
interface, or by a class that implements the
Callable<>
in-
terface when you want the task to return a value.