Chapter 28 The Concurrency Utilities _ Java_ The Complete Reference, Eleventh Edition, 11th Edition
Chapter 28 The Concurrency Utilities _ Java_ The Complete Reference, Eleventh Edition, 11th Edition
28
The Concurrency Utilities
From the start, Java has provided built-in support for multithreading and
synchronization. For example, new threads can be created by implement-
ing Runnable or by extending Thread; synchronization is available by
use of the synchronized keyword; and interthread communication is
supported by the wait( ) and notify( ) methods that are defined by
Object. In general, this built-in support for multithreading was one of
Java’s most important innovations and is still one of its major strengths.
It is important to explain at the outset that many Java programs make use
of multithreading and are, therefore, “concurrent.” However, as it is used
in this chapter, the term concurrent program refers to a program that
makes extensive, integral use of concurrently executing threads. An ex-
ample of such a program is one that uses separate threads to simultane-
ously compute the partial results of a larger computation. Another exam-
ple is a program that coordinates the activities of several threads, each of
which seeks access to information in a database. In this case, read-only
accesses might be handled differently from those that require read/write
capabilities.
Although the original concurrent API was impressive in its own right, it
was significantly expanded by JDK 7. The most important addition was
the Fork/Join Framework. The Fork/Join Framework facilitates the cre-
ation of programs that make use of multiple processors (such as those
found in multicore systems). Thus, it streamlines the development of pro-
grams in which two or more pieces execute with true simultaneity (that
is, true parallel execution), not just time-slicing. As you can easily imag-
ine, parallel execution can dramatically increase the speed of certain op-
erations. Because multicore systems are now commonplace, the inclusion
of the Fork/Join Framework was as timely as it was powerful. With the re-
lease of JDK 8, the Fork/Join Framework was further enhanced.
Furthermore, both JDK 8 and JDK 9 have added features related to other
parts of the concurrent API. Thus, the concurrent API continues to evolve
and expand to meet the needs of the contemporary computing
environment.
The original concurrent API was quite large, and the additions made over
the years have increased its size substantially. As you might expect, many
of the issues surrounding the concurrency utilities are quite complex. It
is beyond the scope of this book to discuss all of its facets. The preceding
notwithstanding, it is important for all programmers to have a general,
working knowledge of key aspects of the concurrent API. Even in pro-
grams that are not intensively parallel, features such as synchronizers,
callable threads, and executors, are applicable to a wide variety of situa-
tions. Perhaps most importantly, because of the rise of multicore comput-
ers, solutions involving the Fork/Join Framework are becoming more
common. For these reasons, this chapter presents an overview of several
core features defined by the concurrency utilities and shows a number of
examples that demonstrate their use. It concludes with an introduction to
the Fork/Join Framework.
java.util.concurrent
• Synchronizers
• Executors
• Concurrent collections
java.util.concurrent.atomic
java.util.concurrent.locks
In general, to use a semaphore, the thread that wants access to the shared
resource tries to acquire a permit. If the semaphore’s count is greater
than zero, then the thread acquires a permit, which causes the
semaphore’s count to be decremented. Otherwise, the thread will be
blocked until a permit can be acquired. When the thread no longer needs
access to the shared resource, it releases the permit, which causes the
semaphore’s count to be incremented. If there is another thread waiting
for a permit, then that thread will acquire a permit at that time. Java’s
Semaphore class implements this mechanism.
Semaphore(int num)
Semaphore(int num, boolean how)
Here, num specifies the initial permit count. Thus, num specifies the num-
ber of threads that can access a shared resource at any one time. If num is
one, then only one thread can access the resource at any one time. By de-
fault, waiting threads are granted a permit in an undefined order. By set-
ting how to true, you can ensure that waiting threads are granted a per-
mit in the order in which they requested access.
To acquire a permit, call the acquire( ) method, which has these two
forms:
void release( )
void release(int num)
The first form releases one permit. The second form releases the number
of permits specified by num.
In both IncThread and DecThread, notice the call to sleep( ) within run(
). It is used to “prove” that accesses to Shared.count are synchronized by
the semaphore. In run( ), the call to sleep( ) causes the invoking thread
to pause between each access to Shared.count. This would normally en-
able the second thread to run. However, because of the semaphore, the
second thread must wait until the first has released the permit, which
happens only after all accesses by the first thread are complete. Thus,
Shared.count is incremented five times by IncThread and decremented
five times by DecThread. The increments and decrements are not
intermixed.
Put: 0
Got: 0
Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5
.
.
.
As you can see, the calls to put( ) and get( ) are synchronized. That is,
each call to put( ) is followed by a call to get( ) and no values are missed.
Without the semaphores, multiple calls to put( ) would have occurred
without matching calls to get( ), resulting in values being missed. (To
prove this, remove the semaphore code and observe the results.)
CountDownLatch
Sometimes you will want a thread to wait until one or more events have
occurred. To handle such a situation, the concurrent API supplies
CountDownLatch. A CountDownLatch is initially created with a count
of the number of events that must occur before the latch is released. Each
time an event happens, the count is decremented. When the count
reaches zero, the latch opens.
CountDownLatch(int num)
Here, num specifies the number of events that must occur in order for the
latch to open.
To wait on the latch, a thread calls await( ), which has the forms shown
here:
The first form waits until the count associated with the invoking
CountDownLatch reaches zero. The second form waits only for the pe-
riod of time specified by wait. The units represented by wait are specified
by tu, which is an object the TimeUnit enumeration. (TimeUnit is de-
scribed later in this chapter.) It returns false if the time limit is reached
and true if the countdown reaches zero.
Each call to countDown( ) decrements the count associated with the in-
voking object.
Starting
0
1
2
3
4
Done
Inside the run( ) method of MyThread, a loop is created that iterates five
times. With each iteration, the countDown( ) method is called on latch,
which refers to cdl in main( ). After the fifth iteration, the latch opens,
which allows the main thread to resume.
CyclicBarrier
Here, numThreads specifies the number of threads that must reach the
barrier before execution continues. In the second form, action specifies a
thread that will be executed when the barrier is reached.
Here is the general procedure that you will follow to use CyclicBarrier.
First, create a CyclicBarrier object, specifying the number of threads
that you will be waiting for. Next, when each thread reaches the barrier,
have it call await( ) on that object. This will pause execution of the thread
until all of the other threads also call await( ). Once the specified number
of threads has reached the barrier, await( ) will return and execution
will resume. Also, if you have specified an action, then that thread is
executed.
The first form waits until all the threads have reached the barrier point.
The second form waits only for the period of time specified by wait. The
units represented by wait are specified by tu. Both forms return a value
that indicates the order that the threads arrive at the barrier point. The
first thread returns a value equal to the number of threads waited upon
minus one. The last thread returns zero.
the following output will be produced. (The precise order in which the
threads execute may vary.)
Starting
A
B
C
Barrier Reached!
X
Y
Z
Barrier Reached!
Exchanger<V>
Got: ABCDE
Got: FGHIJ
Got: KLMNO
In the program, the main( ) method creates an Exchanger for strings.
This object is then used to synchronize the exchange of strings between
the MakeString and UseString classes. The MakeString class fills a
string with data. The UseString exchanges an empty string for a full one.
It then displays the contents of the newly constructed string. The ex-
change of empty and full buffers is synchronized by the exchange( )
method, which is called by both classes’ run( ) method.
Phaser
Phaser defines four constructors. Here are the two used in this section:
Phaser( )
Phaser(int numParties)
The first creates a phaser that has a registration count of zero. The sec-
ond sets the registration count to numParties. The term party is often ap-
plied to the objects that register with a phaser. Although typically there is
a one-to-correspondence between the number of registrants and the
number of threads being synchronized, this is not required. In both
cases, the current phase is zero. That is, when a Phaser is created, it is
initially at phase zero.
In general, here is how you use Phaser. First, create a new instance of
Phaser. Next, register one or more parties with the phaser, either by call-
ing register( ) or by specifying the number of parties in the constructor.
For each registered party, have the phaser wait until all registered parties
complete a phase. A party signals this by calling one of a variety of meth-
ods supplied by Phaser, such as arrive( ) or arriveAndAwaitAdvance( ).
After all parties have arrived, the phase is complete, and the phaser can
move on to the next phase (if there is one), or terminate. The following
sections explain the process in detail.
int register()
int arrive( )
If you want to indicate the completion of a phase and then wait until all
other registrants have also completed that phase, use arriveAndAwait-
Advance( ). It is shown here:
int arriveAndAwaitAdvance( )
It waits until all parties have arrived. It returns the next phase number
or a negative value if the phaser has been terminated. This method
should be called only by a registered party.
int arriveAndDeregister( )
It returns the current phase number or a negative value if the phaser has
been terminated. It does not wait until the phase is complete. This
method should be called only by a registered party.
When a Phaser is created, the first phase will be 0, the second phase 1,
the third phase 2, and so on. A negative value is returned if the invoking
Phaser has been terminated.
Now look at MyThread. First, notice that the constructor is passed a ref-
erence to the phaser that it will use and then registers with the new
thread as a party on that phaser. Thus, each new MyThread becomes a
party registered with the passed-in phaser. Also notice that each thread
has three phases. In this example, each phase consists of a placeholder
that simply displays the name of the thread and what it is doing.
Obviously, in real-world code, the thread would be performing more
meaningful actions. Between the first two phases, the thread calls arrive-
AndAwaitAdvance( ). Thus, each thread waits until all threads have
completed the phase (and the main thread is ready). After all threads
have arrived (including the main thread), the phaser moves on to the
next phase. After the third phase, each thread deregisters itself with a call
to arriveAndDeregister( ). As the comments in MyThread explain, the
calls to sleep( ) are used for the purposes of illustration to ensure that the
output is not jumbled because of the multithreading. They are not needed
to make the phaser work properly. If you remove them, the output may
look a bit jumbled, but the phases will still be synchronized correctly.
One other point: Although the preceding example used three threads that
were all of the same type, this is not a requirement. Each party that uses
a phaser can be unique, with each performing some separate task.
Here, phase will contain the current phase number prior to being incre-
mented and numParties will contain the number of registered parties. To
terminate the phaser, onAdvance( ) must return true. To keep the
phaser alive, onAdvance( ) must return false. The default version of on-
Advance( ) returns true (thus terminating the phaser) when there are no
registered parties. As a general rule, your override should also follow this
practice.
Now, look closely at the code for onAdvance( ). Each time onAdvance( )
is called, it is passed the current phase and the number of registered par-
ties. If the current phase equals the specified phase, or if the number of
registered parties is zero, onAdvance( ) returns true, thus stopping the
phaser. This is accomplished with this line of code:
As you can see, very little code is needed to accommodate the desired
outcome.
Before moving on, it is useful to point out that you don’t necessarily need
to explicitly extend Phaser as the previous example does to simply over-
ride onAdvance( ). In some cases, more compact code can be created by
using an anonymous inner class to override onAdvance( ).
To register more than one party, call bulkRegister( ). To obtain the num-
ber of registered parties, call getRegisteredParties( ). You can also ob-
tain the number of arrived parties and unarrived parties by calling
getArrivedParties( ) and getUnarrivedParties( ), respectively. To force
the phaser to enter a terminated state, call forceTermination( ).
Phaser also lets you create a tree of phasers. This is supported by two ad-
ditional constructors, which let you specify the parent, and the getPar-
ent( ) method.
Using an Executor
void shutdown( )
Before going any further, a simple example that uses an executor will be
of value. The following program creates a fixed thread pool that contains
two threads. It then uses that pool to execute four tasks. Thus, four tasks
share the two threads that are in the pool. After the tasks finish, the pool
is shut down and the program ends.
The output from the program is shown here. (The precise order in which
the threads execute may vary.)
Starting
A: 0
A: 1
A: 2
A: 3
A: 4
C: 0
C: 1
C: 2
C: 3
C: 4
D: 0
D: 1
D: 2
D: 3
D: 4
B: 0
B: 1
B: 2
B: 3
B: 4
Done
As the output shows, even though the thread pool contains only two
threads, all four tasks are still executed. However, only two can run at
the same time. The others must wait until one of the pooled threads is
available for use.
One of the most interesting features of the concurrent API is the Callable
interface. This interface represents a thread that returns a value. An ap-
plication can use Callable objects to compute results that are then re-
turned to the invoking thread. This is a powerful mechanism because it
facilitates the coding of many types of numerical computations in which
partial results are computed simultaneously. It can also be used to run a
thread that returns a status code that indicates the successful completion
of the thread.
interface Callable<V>
Here, V indicates the type of data returned by the task. Callable defines
only one method, call( ), which is shown here:
Inside call( ), you define the task that you want performed. After that
task completes, you return the result. If the result cannot be computed,
call( ) must throw an exception.
Future is a generic interface that represents the value that will be re-
turned by a Callable object. Because this value is obtained at some future
time, the name Future is appropriate. Future is defined like this:
interface Future<V>
To obtain the returned value, you will call Future’s get( ) method, which
has these two forms:
V get( )
throws InterruptedException, ExecutionException
The first form waits for the result indefinitely. The second form allows
you to specify a timeout period in wait. The units of wait are passed in tu,
which is an object of the TimeUnit enumeration, described later in this
chapter.
Starting
55
5.0
120
Done
DAYS
HOURS
MINUTES
SECONDS
MICROSECONDS
MILLISECONDS
NANOSECONDS
Although TimeUnit lets you specify any of these values in calls to meth-
ods that take a timing argument, there is no guarantee that the system is
capable of the specified resolution.
The convert( ) method converts tval into the specified unit and returns
the result. The to methods perform the indicated conversion and return
the result. To these methods, JDK 9 added the methods toChronoUnit( )
and of( ), which convert between java.time.temporal.ChronoUnits and
TimeUnits. JDK 11 adds another version of convert( ) that converts a
java.time.Duration object into a long.
TimeUnit also defines the following timing methods:
Here, sleep( ) pauses execution for the specified delay period, which is
specified in terms of the invoking enumeration constant. It translates into
a call to Thread.sleep( ). The timedJoin( ) method is a specialized ver-
sion of Thread.join( ) in which thrd pauses for the time period specified
by delay, which is described in terms of the invoking time unit. The
timedWait( ) method is a specialized version of Object.wait( ) in which
obj is waited on for the period of time specified by delay, which is de-
scribed in terms of the invoking time unit.
ArrayBlockingQueue
ConcurrentHashMap
ConcurrentLinkedDeque ConcurrentLinkedQueue
ConcurrentSkipListMap
ConcurrentSkipListSet
CopyOnWriteArrayList
CopyOnWriteArraySet
DelayQueue
LinkedBlockingDeque
LinkedBlockingQueue
LinkedTransferQueue PriorityBlockingQueue
SynchronousQueue
Locks
Locks are particularly useful when multiple threads need to access the
value of shared data. For example, an inventory application might have a
thread that first confirms that an item is in stock and then decreases the
number of items on hand as each sale occurs. If two or more of these
threads are running, then without some form of synchronization, it
would be possible for one thread to be in the middle of a transaction
when the second thread begins its transaction. The result could be that
both threads would assume that adequate inventory exists, even if there
is only sufficient inventory on hand to satisfy one sale. In this type of situ-
ation, a lock offers a convenient means of handling the needed
synchronization.
The Lock interface defines a lock. The methods defined by Lock are
shown in Table 28-1. In general, to acquire a lock, call lock( ). If the lock
is unavailable, lock( ) will wait. To release a lock, call unlock( ). To see if
a lock is available, and to acquire it if it is, call tryLock( ). This method
will not wait for the lock if it is unavailable. Instead, it returns true if the
lock is acquired and false otherwise. The newCondition( ) method re-
turns a Condition object associated with the lock. Using a Condition, you
gain detailed control of the lock through methods such as await( ) and
signal( ), which provide functionality similar to Object.wait( ) and
Object.notify( ).
Table 28-1 The Lock Methods
Starting A
A is waiting to lock count.
A is locking count.
A: 1
A is sleeping.
Starting B
B is waiting to lock count.
A is unlocking count.
B is locking count.
B: 2
B is sleeping.
B is unlocking count.
Atomic Operations
ForkJoinTask<V>
ForkJoinTask defines many methods. At the core are fork( ) and join( ),
shown here:
final V join( )
The fork( ) method submits the invoking task for asynchronous execu-
tion of the invoking task. This means that the thread that calls fork( )
continues to run. The fork( ) method returns this after the task is sched-
uled for execution. Prior to JDK 8, fork( ) could be executed only from
within the computational portion of another ForkJoinTask, which is run-
ning within a ForkJoinPool. (You will see how to create the computa-
tional portion of a task shortly.) However, with the advent of JDK 8, if
fork( ) is not called while executing within a ForkJoinPool, then a com-
mon pool is automatically used. The join( ) method waits until the task
on which it is called terminates. The result of the task is returned. Thus,
through the use of fork( ) and join( ), you can start one or more new
tasks and then wait for them to finish.
final V invoke( )
You can invoke more than one task at a time by using invokeAll( ). Two
of its forms are shown here:
In the first case, taskA and taskB are executed. In the second case, all
specified tasks are executed. In both cases, the calling thread waits until
all of the specified tasks have terminated. Prior to JDK 8, the invokeAll( )
method could be executed only from within the computational portion of
another ForkJoinTask, which is running within a ForkJoinPool. JDK 8’s
inclusion of the common pool relaxed this requirement.
RecursiveAction
RecursiveTask<V>
ForkJoinPool
ForkJoinPool( )
ForkJoinPool(int pLevel)
The first creates a default pool that supports a level of parallelism equal
to the number of processors available in the system. The second lets you
specify the level of parallelism. Its value must be greater than zero and
not more than the limits of the implementation. The level of parallelism
determines the number of threads that can execute concurrently. As a re-
sult, the level of parallelism effectively determines the number of tasks
that can be executed simultaneously. (Of course, the number of tasks that
can execute simultaneously cannot exceed the number of processors.) It
is important to understand that the level of parallelism does not, how-
ever, limit the number of tasks that can be managed by the pool. A
ForkJoinPool can manage many more tasks than its level of parallelism.
Also, the level of parallelism is only a target. It is not a guarantee.
After you have created an instance of ForkJoinPool, you can start a task
in a number of different ways. The first task started is often thought of as
the main task. Frequently, the main task begins subtasks that are also
managed by the pool. One common way to begin a main task is to call in-
voke( ) on the ForkJoinPool. It is shown here:
This method begins the task specified by task, and it returns the result of
the task. This means that the calling code waits until invoke( ) returns.
To start a task without waiting for its completion, you can use execute( ).
Here is one of its forms:
void execute(ForkJoinTask<?> task)
In this case, task is started, but the calling code does not wait for its com-
pletion. Rather, the calling code continues execution asynchronously.
There are two basic ways to start a task using the common pool. First,
you can obtain a reference to the pool by calling commonPool( ) and
then use that reference to call invoke( ) or execute( ), as just described.
Second, you can call ForkJoinTask methods such as fork( ) or invoke( )
on the task from outside its computational portion. In this case, the com-
mon pool will automatically be used. In other words, fork( ) and invoke(
) will start a task using the common pool if the task is not already run-
ning within a ForkJoinPool.
As you can see, the values of the array elements have been transformed
into their square roots.
Let’s look closely at how this program works. First, notice that
SqrtTransform is a class that extends RecursiveAction. As explained,
RecursiveAction extends ForkJoinTask for tasks that do not return re-
sults. Next, notice the final variable seqThreshold. This is the value that
determines when sequential processing will take place. This value is set
(somewhat arbitrarily) to 1,000. Next, notice that a reference to the array
to be processed is stored in data and that the fields start and end are
used to indicate the boundaries of the elements to be accessed.
task.invoke();
As this discussion shows, the common pool can be easier to use than cre-
ating your own pool. Furthermore, in many cases, the common pool is
the preferable approach.
Before moving on, it is important to understand the impact that the level
of parallelism has on the performance of a fork/join task and how the
parallelism and the threshold interact. The program shown in this section
lets you experiment with different degrees of parallelism and threshold
values. Assuming that you are using a multicore computer, you can inter-
actively observe the effect of these values.
ForkJoinPool(int pLevel)
Here, pLevel specifies the level of parallelism, which must be greater than
zero and less than the implementation defined limit.
To give you an idea of the difference that parallelism makes, try this ex-
periment. First, execute the program like this:
Level of parallelism: 1
Sequential threshold: 1000
Elapsed time: 259677487 ns
Here is sample output from this run produced by the same dual-core
computer:
Level of parallelism: 2
Sequential threshold: 1000
Elapsed time: 169254472 ns
Here are two other methods that you might find useful when experiment-
ing with the execution characteristics of a fork/join program. First, you
can obtain the level of parallelism by calling getParallelism( ), which is
defined by ForkJoinPool. It is shown here:
int getParallelism( )
It returns the parallelism level currently in effect. Recall that for pools
that you create, by default, this value will equal the number of available
processors. (To obtain the parallelism level for the common pool, you can
also use getCommonPoolParallelism( ). Second, you can obtain the
number of processors available in the system by calling availableProces-
sors( ), which is defined by the Runtime class. It is shown here:
int availableProcessors( )
The value returned may change from one call to the next because of
other system demands.
Summation -2500.0
There are a couple of interesting items in this program. First, notice that
the two subtasks are executed by calling fork( ), as shown here:
subTaskA.fork();
subTaskB.fork();
In this case, fork( ) is used because it starts a task but does not wait for it
to finish. (Thus, it asynchronously runs the task.) The result of each task
is obtained by calling join( ), as shown here:
There are other ways to approach the handling of the asynchronous exe-
cution of the subtasks. For example, the following sequence uses fork( )
to start subTaskA and uses invoke( ) to start and wait for subTaskB:
subTaskA.fork();
sum = subTaskB.invoke() + subTaskA.join();
subTaskA.fork();
sum = subTaskB.compute() + subTaskA.join();
In both forms, task specifies the task to run. Notice that the second form
lets you specify a Runnable rather than a ForkJoinTask task. Thus, it
forms a bridge between Java’s traditional approach to multithreading
and the Fork/Join Framework. It is important to remember that the
threads used by a ForkJoinPool are daemon. Thus, they will end when
the main thread ends. As a result, you may need to keep the main thread
alive until the tasks have finished.
Cancelling a Task
It returns true if the invoking task has been cancelled prior to comple-
tion and false otherwise.
It returns true if the invoking task completed normally, that is, if it did
not throw an exception and it was not cancelled via a call to cancel( ). It
returns false otherwise.
Restarting a Task
Normally, you cannot rerun a task. In other words, once a task completes,
it cannot be restarted. However, you can reinitialize the state of the task
(after it has completed) so it can be run again. This is done by calling
reinitialize( ), as shown here:
void reinitialize( )
This method resets the state of the invoking task. However, any modifica-
tion made to any persistent data that is operated upon by the task will not
be undone. For example, if the task modifies an array, then those modifi-
cations are not undone by calling reinitialize( ).
Things to Explore
In some cases, you will want to ensure that methods such as invokeAll( )
and fork( ) are called only from within a ForkJoinTask. This is usually a
simple matter, but occasionally, you may have code that can be executed
from either inside or outside a task. You can determine if your code is ex-
ecuting inside a task by calling inForkJoinPool( ).
You can convert a Runnable or Callable object into a ForkJoinTask by
using the adapt( ) method defined by ForkJoinTask. It has three forms,
one for converting a Callable, one for a Runnable that does not return a
result, and one for a Runnable that does return a result. In the case of a
Callable, the call( ) method is run. In the case of Runnable, the run( )
method is run.
You can obtain an approximate count of the number of tasks that are in
the queue of the invoking thread by calling getQueuedTaskCount( ). You
can obtain an approximate count of how many tasks the invoking thread
has in its queue that are in excess of the number of other threads in the
pool that might “steal” them, by calling getSurplusQueuedTaskCount( ).
Remember, in the Fork/Join Framework, work-stealing is one way in
which a high level of efficiency is obtained. Although this process is auto-
matic, in some cases, the information may prove helpful in optimizing
through-put.
When you run the program, you will see a series of messages on the
screen that describe the state of the pool. Here is an example of one. Of
course, your output may vary, based on the number of processors,
threshold values, task load, and so on.
java.util.concurrent.ForkJoinPool@141d683[Running, parallelism
= 2,
size = 2, active = 0, running = 2, steals = 0, tasks = 0, sub‐
missions = 1]
You can obtain the number of worker threads currently in the pool by
calling getPoolSize( ). You can obtain an approximate count of the active
threads in the pool by calling getActiveThreadCount( ).
To shut down a pool, call shutdown( ). Currently active tasks will still be
executed, but no new tasks can be started. To stop a pool immediately,
call shutdownNow( ). In this case, an attempt is made to cancel currently
active tasks. (It is important to point out, however, that neither of these
methods affects the common pool.) You can determine if a pool is shut
down by calling isShutdown( ). It returns true if the pool has been shut
down and false otherwise. To determine if the pool has been shut down
and all tasks have been completed, call isTerminated( ).
Here are a few tips to help you avoid some of the more troublesome pit-
falls associated with using the Fork/Join Framework. First, avoid using a
sequential threshold that is too low. In general, erring on the high side is
better than erring on the low side. If the threshold is too low, more time
can be consumed generating and switching tasks than in processing the
tasks. Second, usually it is best to use the default level of parallelism. If
you specify a smaller number, it may significantly reduce the benefits of
using the Fork/Join Framework.
One last point: Except under unusual circumstances, do not make as-
sumptions about the execution environment that your code will run in.
This means you should not assume that some specific number of proces-
sors will be available, or that the execution characteristics of your pro-
gram won’t be affected by other processes running at the same time.