Java Platform, Standard Edition (Java SE) 11
Official website DEV
Java® Language Specification (Java SE 11 Edition)
The Java Tutorial is written for JDK 8. The examples and practices described on this page do not take advantage of improvements introduced in later releases, and may use techniques that are no longer available.
Computer users take for granted that their systems can do more than one thing at a time. They figured they could keep working in their word processor while other apps downloaded files, managed print queues, and streamed audio. Even a single application is often expected to do more than one thing at a time. For example, a streaming audio application must simultaneously read digital audio from the network, decompress it, manage playback, and update its display. Even a word processor should always be ready to respond to keyboard and mouse events, no matter how busy it is, to reformat text or update the display. Software that can do these things is called concurrent software ( concurrent software
).
The Java platform was designed from the ground up to support concurrent programming, with basic concurrency support in the Java programming language and Java class libraries. Beginning with version 5.0, the Java platform also includes advanced concurrency APIs. This lesson introduces the platform's basic concurrency support and summarizes java.util.concurrent
some of the high-level APIs in the package.
1. Process and thread
In concurrent programming, there are two basic execution units: process ( processes
) and thread ( threads
). In the Java programming language, concurrent programming is mostly concerned with threads . However, progress is also important.
A computer system typically has many active processes and threads. This is true even in systems with only one execution core, so only one thread is actually executing at any given moment. time slicing
The processing time of a single core is shared between processes and threads through an operating system feature called time slicing ( ).
It is becoming more common for computer systems to have multiple processors or processors with multiple execution cores. This greatly enhances the system's ability to execute processes and threads concurrently -- but concurrency is possible even on simple systems without multiple processors or execution cores.
1.1 Process
A process has a self-contained execution environment. A process usually has a complete, private set of basic runtime resources; in particular, each process has its own memory space.
进程
Often regarded as a synonym for 程序
or应用程序
. However, what a user sees as a single application may actually be a set of cooperating processes . To facilitate inter-process communication, most operating systems support inter-process communication (Inter Process Communication, IPC) resources, such as pipes ( pipe
) and sockets ( socket
). IPC is used not only for communication between processes on the same system, but also between processes on different systems.
Most implementations of the Java virtual machine run as a single process . A Java application can create additional processes using the ProcessBuilder object. Multi-process applications are beyond the scope of this lesson.
1.2 Threads
Threads are sometimes called lightweight processes ( lightweight processes
) . Both processes and threads provide an execution environment, but creating a new thread requires fewer resources than creating a new process .
Threads exist within processes - each process has at least one thread . Threads share the resources of the process, including memory and open files. This makes communication more efficient, but also potentially problematic.
Multithreaded execution is a fundamental feature of the Java platform . Every application has at least one thread -- at least a few, if you count "system" threads that perform tasks like memory management and signal handling. But from an application programmer's point of view, you start with only one thread, called the main thread ( main thread
) . This thread is able to create other threads, which we will demonstrate in the next section.
2. Thread object
Each thread is associated with an instance of the Thread class. Thread
There are two basic strategies for creating concurrent applications using objects.
- To directly control thread creation and management, simply instantiate a each time your application needs to start an asynchronous task
Thread
. - To abstract thread management from the rest of the application, pass the application's tasks to an Executor(
executor
).
This section documents Thread
the use of the objects. Executors ( executor
) are discussed along with other higher-order concurrency objects .
2.1 Define and start a thread
The application that creates Thread
the instance must provide code that will run in that thread. There are two methods:
- Provide an
Runnable
object. The Runnable interface defines a methodrun
used to contain the code executed in the thread.Runnable
The object is passed toThread
the constructor, as in the HelloRunnable example:
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}
}
- subclass
Thread
.Thread
The class itself implementsRunnable
even though itsrun
methods do nothing. Applications can inheritThread
and provide their ownrun
implementation, such as the HelloThread example:
public class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new HelloThread()).start();
}
}
Note that both examples call Thread.start
to start a new thread.
Which style should you use? The first idiom uses Runnable
objects, which are more general because Runnable
objects can inherit Thread
from other classes . The second idiom is easier to use in simple applications, but it is Thread
limited by the fact that the task class must be a descendant of . This lesson focuses on the first approach, which separates Runnable
the task from the Thread
objects that perform it. This method is not only more flexible, but also suitable for the high-level thread management API described later.
Thread
The class defines a number of methods useful for thread management. These methods include static methods that provide information about or affect the state of the thread calling the method. Other methods are called by the management thread and Thread
other threads involved with the object. We'll examine some of these methods in the following sections.
2.2 Use Sleep to suspend execution
Thread.sleep
Causes the current thread to suspend execution for a specified period of time . This is an efficient way to give processor time to other threads of an application or other applications that may be running on the computer system . sleep
method can also be used to adjust the speed (as shown in the example below), and to wait for another thread that needs time for a task (as shown in the example in a later subsection SimpleThreads
).
Two overloaded versions are provided sleep
: one specifying the sleep time in milliseconds and the other specifying the sleep time in nanoseconds. However, these sleep times are not guaranteed to be precise , as they are limited by the capabilities provided by the underlying operating system. Additionally, sleep cycles can interrupts
be terminated via interrupt() , which we'll see in a later section. Under no circumstances can it be assumed that the call sleep
will suspend the thread for the specified period of time.
The SleepMessages example uses to sleep
print messages every 4 seconds:
public class SleepMessages {
public static void main(String args[])
throws InterruptedException {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
for (int i = 0;
i < importantInfo.length;
i++) {
//Pause for 4 seconds
Thread.sleep(4000);
//Print a message
System.out.println(importantInfo[i]);
}
}
}
Note main
the statement that it throws InterruptedException
. This is the exception thrown when another thread sleep
interrupts the current thread while it is active . sleep
Since this application does not define another thread that caused the interrupt, it does not need to catch InterruptedException
.
2.3 Interrupts
An interrupt is an indication to a thread that it should stop what it is doing and do something else . It's up to the programmer to decide how exactly a thread responds to interrupts, but thread termination is very common. This is the usage emphasized in this lesson.
A thread sends an interrupt by calling interrupt on the interrupted thread Thread
object . For the interrupt mechanism to work properly, the interrupted thread must support its own interrupts.
support interrupt
线程如何支持自己的中断?
It depends on what it's currently doing . If the thread calls the throwing method frequently InterruptedException
, it only needs to return from the run method after catching the exception . For example, suppose SleepMessages
the central message loop in the example is in a method Runnable
of the thread's object run
. It can then be modified like this to support interrupts:
for (int i = 0; i < importantInfo.length; i++) {
// Pause for 4 seconds
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// We've been interrupted: no more messages.
return;
}
// Print a message
System.out.println(importantInfo[i]);
}
Many InterruptedException
methods that throw (eg sleep
) are designed to cancel their current operation and return immediately when an interrupt is received.
InterruptedException
What if the thread doesn't call the throwing method for a long timeThread.interrupted
? It then has to call it periodically and return if an interrupt is received true
. For example:
for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
// We've been interrupted: no more crunching.
return;
}
}
In this simple example, the code just tests for interrupts, and exits the thread when an interrupt is received. In more complex applications, InterruptedException
it might make more sense to throw:
if (Thread.interrupted()) {
throw new InterruptedException();
}
This allows interrupt handling code to be concentrated in catch
clauses.
interrupt status flag
The interrupt mechanism is interrupt status
implemented using an internal flag called the interrupt status ( ). Call Thread.interrupt
to set this flag. When a thread checks for an interrupt by calling a static method Thread.interrupted
, the interrupt status will be cleared . The non-static isInterrupted
method is used by one thread to query the interrupt status of another thread, it does not change the interrupt status flag.
By convention, any InterruptedException
method that exits by throwing clears the interrupt status on exit. However, there is always the possibility that the interrupt status will interrupt
be set again immediately by another calling thread.
2.4 Joins
join
method allows one thread to wait for the completion of another thread . If t
it is an object that a thread is executing Thread
,
t.join();
Causes the current thread to suspend execution until t
the thread terminates. join
The overload of allows the programmer to specify a wait time . However, as with sleep
, join
relies on the timing of the operating system, so you should not assume that you join
will wait exactly as you specify.
Similar to sleep
, respond to interrupts join
by using InterruptedException
exit.
Example of SimpleThreads
The following example brings together some of the concepts from this section. SimpleThreads consists of two threads. The first is the main thread that every Java application has. The main thread creates a new thread from Runnable
the object, , and waits for it to complete. MessageLoop
If MessageLoop
a thread takes too long to complete, the main thread interrupts it.
MessageLoop
The thread prints out a series of messages. If interrupted before printing all messages, MessageLoop
the thread will print a message and exit.
public class SimpleThreads {
// Display a message, preceded by
// the name of the current thread
static void threadMessage(String message) {
String threadName =
Thread.currentThread().getName();
System.out.format("%s: %s%n",
threadName,
message);
}
private static class MessageLoop
implements Runnable {
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
try {
for (int i = 0;
i < importantInfo.length;
i++) {
// Pause for 4 seconds
Thread.sleep(4000);
// Print a message
threadMessage(importantInfo[i]);
}
} catch (InterruptedException e) {
threadMessage("I wasn't done!");
}
}
}
public static void main(String args[])
throws InterruptedException {
// Delay, in milliseconds before
// we interrupt MessageLoop
// thread (default one hour).
long patience = 1000 * 60 * 60;
// If command line argument
// present, gives patience
// in seconds.
if (args.length > 0) {
try {
patience = Long.parseLong(args[0]) * 1000;
} catch (NumberFormatException e) {
System.err.println("Argument must be an integer.");
System.exit(1);
}
}
threadMessage("Starting MessageLoop thread");
long startTime = System.currentTimeMillis();
Thread t = new Thread(new MessageLoop());
t.start();
threadMessage("Waiting for MessageLoop thread to finish");
// loop until MessageLoop
// thread exits
while (t.isAlive()) {
threadMessage("Still waiting...");
// Wait maximum of 1 second
// for MessageLoop thread
// to finish.
t.join(1000);
if (((System.currentTimeMillis() - startTime) > patience)
&& t.isAlive()) {
threadMessage("Tired of waiting!");
t.interrupt();
// Shouldn't be long now
// -- wait indefinitely
t.join();
}
}
threadMessage("Finally!");
}
}
3. Synchronization
Threads communicate primarily by sharing access to fields and references to objects referenced by fields . This form of communication is very efficient, but two types of errors can occur: thread interference ( thread interference
) and memory consistency errors ( memory consistency errors
). The tool needed to prevent these errors is sync( synchronization
).
However, synchronization may introduce thread contention, which occurs when two or more threads try to access the same resource at the same time, and causes the Java runtime to execute one or more threads slower, or even suspend their execution. Starvation and livelock are forms of thread contention. See the Lifecycle section for more information.
This section covers the following topics:
- Thread Interference : Describes how errors can be introduced when multiple threads access shared data.
- Memory consistency errors describe errors due to inconsistent views of shared memory.
- Synchronized methods describe a simple idiom that is effective against thread interference and memory consistency errors.
- Implicit Locks and Synchronization describes a more general style of synchronization and describes how to implement synchronization based on implicit locks.
- Atomic access discusses the general idea of operations that cannot be interfered with by other threads.
3.1 Thread Interference
Consider a simple class called Counter
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
Counter
is designed to increment
increment c by 1 and decrement
decrement c by 1 for each call. However, if an Counter
object is referenced by multiple threads, interference between threads may prevent this from happening as desired.
Interference occurs when two operations run in different threads but interleave on the same data . This means that the two operations consist of multiple steps and the sequences of steps overlap.
Counter
It seems impossible for operations on an instance to interleave, since c
both operations on are single simple statements. However, even simple statements can be translated into multiple steps by the virtual machine . We will not examine the specific steps taken by the virtual machine - c++;
it is enough to know that a single expression can be broken down into three steps:
- Retrieve the current value of c.
- Increment the retrieved value by 1.
- Store the incremented value back in c.
Expressionsc--;
can be decomposed in the same way, except the second step is decremented instead of incremented.
Assume that while thread A is calling increment
, thread B calls decrement
. If c
the initial value of 0, their interleaving actions may follow the following order:
- Thread A: Retrieve c.
- Thread B: Retrieve c.
- Thread A: Increment retrieved value; result is 1.
- Thread B: Decrement retrieved value; result is -1.
- Thread A: Store result in c; c is now 1.
- Thread B: Store result in c; c is now -1.
The result of thread A is lost and overwritten by thread b. This particular interleaving is just one possibility. Under different circumstances, it may be that the result of thread B is lost, or there is no error at all. Because thread interference errors are unpredictable, they are difficult to detect and fix.
3.2 Memory Consistency Error
Memory consistency errors
A memory consistency error ( ) occurs when different threads have inconsistent views of what should be the same data . The causes of memory consistency errors are complex and beyond the scope of this tutorial. Fortunately, programmers don't need to understand these reasons in detail. All it takes is a strategy to avoid them.
The key to avoiding memory consistency errors is understanding happens-before
relationships . 这种关系只是保证一个特定语句的内存写入对另一个特定语句可见
. To understand this, consider the following example. Suppose a simple int
field is defined and initialized:
int counter = 0;
counter
Fields are shared between two threads A and B. Suppose thread A increments counter
:
counter++;
Then, shortly after, thread B prints out counter
:
System.out.println(counter);
If these two statements are executed in the same thread, then it is safe to assume that the output value is " 1
". However, if these two statements were executed in separate threads, the value of the output would likely be " 0
", because there is no guarantee that thread A 's counter
changes to thread B will be visible to thread B -- unless the programmer intervenes between the two statements A happens-before relationship is established.
There are several ways to create "happens-before" relationships. One of them is synchronization , which we will see in the next few sections.
We've seen two actions that create "happens-before" relationships.
Thread.start
Every statement that has a happens-before relationship with that statement also has a happens-before relationship with every statement executed by the new thread when the statement is invoked . The effects of code that caused a new thread to be created are visible to the new thread.- When a thread terminates and causes another thread
Thread.join
to return, then all statements executed by the terminating thread have a happens-before relationship with all statements after a successful connection. The thread executing the join can now see the effects of the code in the thread.
See the java.util.concurrent package's Summary page for a list of operations that create a happens-before relationship .
3.3 Synchronization method
The Java programming language provides two basic synchronization idioms: synchronized methods and synchronized statements . The more complex synchronization statements of the two are described in the next section. This section describes the synchronization methods.
To make a method synchronous, simply add the keyword to its declaration synchronized
:
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
If count
it is SynchronizedCounter
an instance of , then making these methods synchronized has two effects:
- First, it is impossible for two calls to a synchronized method on the same object to interleave . When one thread executes a synchronized method for an object, all other threads calling synchronized methods on the same object block (suspend execution) until the first thread finishes processing the object.
- Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent calls to synchronized methods on the same object .
这保证了对象状态的改变对所有线程都是可见的
.
Note that constructors cannot be synchronized - using keywords in constructors synchronized
will result in a syntax error. Synchronous constructors don't make sense because only the thread that created the object can access it while the object is being constructed.
Warning:
在构造将在线程之间共享的对象时,要非常小心,以免对该对象的引用过早地“泄漏”
. For example, suppose you wish to maintain a named classinstances
thatList
contains each instance of the class. You may want to add the following line of code to the constructor: However, other threads can access the object
instances.add(this);
until construction of the object is complete .instances
Synchronized methods support a simple strategy to prevent thread interference and memory consistency errors: If an object is visible to multiple threads, then all reads and writes to variables of that object are done via synchronization synchronized
. ( One important exception: final
fields, which cannot be modified after object construction, can be safely read by unsynchronized methods once the object has been constructed. ) This strategy works, but there can be liveness issues, which we will discuss in this lesson See you later.
3.4 Internal locks and synchronization
Synchronization is built around internal entities called intrinsic locks or monitor locks. (API specifications often refer to this entity simply as a "monitor") Internal locks play a role in both aspects of synchronization: enforcing exclusive access to an object's state, and establishing the happens-before relationship that is critical to visibility.
Every object has an internal lock associated with it . By convention, threads that require exclusive and consistent access to object fields must acquire the object's intrinsic lock before accessing them, and then release the intrinsic lock when they are done using them. A thread owns the lock between acquiring the lock and releasing it. As long as a thread owns an internal lock, no other thread can acquire the lock. It blocks while another thread tries to acquire the lock.
When a thread releases an internal lock, a happens-before relationship is established between that operation and any subsequent acquisition of that lock.
Locks in synchronized methods
When a thread calls a synchronized method, it automatically acquires an internal lock on the method object and releases the lock when the method returns. Lock release occurs even if the return was caused by an uncaught exception.
You might be wondering what happens when a static synchronized method is called, since static methods are associated with a class rather than an object. In this case, the thread acquires Class
an intrinsic lock on the object associated with the class. Therefore, access to a static field of a class is controlled by a lock that is distinct from that of any instance of the class.
synchronous statement
Another way to create synchronous code is to use synchronized statements . Unlike synchronized methods, synchronized statements must specify an object that provides an internal lock:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
In this case, addName
the method needs to synchronize changes to lastName
and nameCount
, but also needs to avoid calling other object methods synchronously. (Calling methods of other objects from synchronized code can create problems that are described in the livveness section.) Without a synchronized statement, there must be a separate non-synchronized method whose sole purpose is to call nameList.add
.
Synchronized statements are also useful for increasing concurrency through fine-grained synchronization . For example, suppose MsLunch
a class has two instance fields c1
and c2
, which are never used together. All updates to these fields must be synchronized, but there's no reason to prevent updates to c1 from being interleaved with updates to c2 -- doing so would create unnecessary blocking, reducing concurrency. Instead of using synchronized methods and this
locks associated with , we created two objects to provide locks individually.
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
Be very careful with this idiom. You have to be absolutely sure that cross accessing the affected fields is indeed safe.
Reentrant synchronization
Recall that a thread cannot acquire a lock owned by another thread. 但是线程可以获得它已经拥有的锁
. Reentrant synchronization ( reentrant synchronization
) is achieved by allowing a thread to acquire the same lock multiple times. This describes a situation where synchronized code directly or indirectly calls a method that also contains synchronized code, and both sets of code use the same lock. Without reentrant synchronization, synchronous code would have to take many extra precautions to avoid threads blocking themselves.
3.5 Atomic access
In programming, atomic operations are operations that occur effectively simultaneously. An atomic operation doesn't stop in the middle: it either happens completely, or it doesn't happen at all . The side effects of an atomic operation are not visible until the operation completes.
We've already seen that incrementing expressions (such as c++
) are not described as atomic operations. Even very simple expressions can define complex operations that can be decomposed into other operations. However, you can specify atomic operations:
- For reference variables and most primitive variables (all types except
long
anddouble
types), reads and writes are atomic. - Reads and writes are atomic for all
volatile
variables declared as (includinglong
and ).double
Atomic operations cannot be interleaved, so they can be used without worrying about thread interference . However, this does not remove all need to synchronize atomic operations, as memory consistency errors can still occur . Using volatile
variables reduces the risk of memory consistency errors because volatile
any write to a variable establishes a happens-before relationship with subsequent reads of that variable. This means that volatile
changes to variables are always visible to other threads. Furthermore, this also means that when a thread reads a volatile
variable, it sees not only volatile
the latest change to that variable, but also the side effects of the code that caused the change.
Using simple atomic variable accesses is more efficient than accessing these variables through synchronized code, but requires more care from the programmer to avoid memory consistency errors. Whether the extra effort is worth it depends on the size and complexity of the application.
Some classes in the java.util.concurrent package provide atomic methods that do not depend on synchronization. We'll discuss them in the section on Higher-Order Concurrency Objects .
4. Viability
The ability of a concurrent application to execute in a timely manner is called its liveness. This section describes the most common survival problem, deadlock , and then briefly describes two other survival problems, starvation and livelock.
4.1 Deadlock
A deadlock describes a situation where two or more threads are permanently blocked, waiting for each other . Here is an example.
Alphonse and Gaston are friends, and they both believe in good manners. A strict rule of good manners is that when you bow to a friend, you must keep bowing until your friend has a chance to return the bow. Unfortunately, this rule does not take into account the possibility of two friends bowing to each other at the same time. This sample application Deadlock simulates this possibility:
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s"
+ " has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s"
+ " has bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse =
new Friend("Alphonse");
final Friend gaston =
new Friend("Gaston");
new Thread(new Runnable() {
public void run() {
alphonse.bow(gaston); }
}).start();
new Thread(new Runnable() {
public void run() {
gaston.bow(alphonse); }
}).start();
}
}
When Deadlock
running, both threads will most likely block trying to call bowBack. Neither block ends because each thread is waiting for the other to exit.
4.2 Starvation and livelock
Starvation and livelocks are less common than deadlocks, but are still problems that every concurrent software designer can encounter.
hunger
Starvation ( Starvation
) describes a situation where a thread cannot gain regular access to a shared resource and cannot make progress . This happens when a shared resource is held unavailable for a long time by a "greedy" thread. For example, suppose an object provides a synchronous method that usually takes a long time to return. If a thread calls this method frequently, other threads that also frequently synchronize access to the same object will usually be blocked.
livelock
A thread often responds to the actions of another thread. If another thread's operation is also a response to another thread's operation, then a livelock may result. As with a deadlock, a thread that is livelocked cannot perform further processing . However, the threads aren't blocked - they're just too busy responding to each other to get back to work. It's like two people overtaking each other in a hallway: Alphonse moves to his left to let Gaston pass, and Gaston moves to the right to let Alphonse pass. Seeing that they were still blocking each other, Alphone moved to his right, and Gaston moved to his left. They're still blocking each other, so...
5. Protection block
Threads often need to coordinate their actions. The most common form of coordination is the guarded block. Such a block first polls for a condition, which must be true before the block can continue. In order to do this properly, there are a few steps that need to be followed.
For example, suppose guardedJoy
it is a method that must be performed after another thread sets the shared variable joy. In theory, such a method could simply loop until the condition is met, but such a loop is wasteful because it executes continuously while waiting.
public void guardedJoy() {
// Simple loop guard. Wastes
// processor time. Don't do this!
while(!joy) {
}
System.out.println("Joy has been achieved!");
}
A more effective guard is to call Object.wait to suspend the current thread. wait
The call will not return until another thread notifies that some special event may have occurred - although not necessarily the event this thread is waiting for:
public synchronized void guardedJoy() {
// This guard only loops once for each special event, which may not
// be the event we're waiting for.
while(!joy) {
try {
wait();
} catch (InterruptedException e) {
}
}
System.out.println("Joy and efficiency have been achieved!");
}
Note: Always called in a loop that tests the wait condition
wait
. Don't assume the interrupt is for the specific condition you're waiting on, or that the condition is still true.
Like many methods that suspend execution, wait
can throw InterruptedException
. In this example, we can ignore the exception - we only care about joy
the value.
Why is this version guardedJoy
synchronous? Assume d
is the object we used to call wait
. An intrinsic lock that a thread d.wai
must own when it calls t , otherwise an error is thrown. d
Calling in a synchronized method wait
is an easy way to acquire this internal lock .
When called wait
, the thread releases the lock and suspends execution . At some time in the future, another thread will acquire the same lock and call Object.notifyAll , notifying all threads waiting on the lock that something important happened:
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
Some time after the second thread releases the lock, the first thread reacquires the lock and wait
continues execution by calling return.
Note: There is also a second notify method
notify
, which wakes up a single thread. Becausenotify
it doesn't allow you to specify the thread that gets woken up, it's only useful in massively parallel applications -- that is, programs with a large number of threads, all doing similar work. In such an application, you don't care which thread is woken up.
Let's use guard blocks to create a Producer-Consumer application. This type of application shares data between two threads: the producer ( producer
) that creates the data, and the consumer ( consumer ) that processes it. The two threads communicate using a shared object. Coordination is essential: the consumer thread cannot attempt to retrieve data until the producer thread has delivered it, nor can the producer thread attempt to deliver new data if the consumer has not retrieved the old data.
In this example, the data is a series of text messages shared via objects of type Drop :
public class Drop {
// Message sent from producer
// to consumer.
private String message;
// True if consumer should wait
// for producer to send message,
// false if producer should wait for
// consumer to retrieve message.
private boolean empty = true;
public synchronized String take() {
// Wait until message is
// available.
while (empty) {
try {
wait();
} catch (InterruptedException e) {
}
}
// Toggle status.
empty = true;
// Notify producer that
// status has changed.
notifyAll();
return message;
}
public synchronized void put(String message) {
// Wait until message has
// been retrieved.
while (!empty) {
try {
wait();
} catch (InterruptedException e) {
}
}
// Toggle status.
empty = false;
// Store message.
this.message = message;
// Notify consumer that status
// has changed.
notifyAll();
}
}
The producer thread defined in Producer sends a series of familiar messages. The string " DONE
" indicates that all messages have been sent. To simulate the unpredictability of real applications, producer threads pause randomly between messages.
import java.util.Random;
public class Producer implements Runnable {
private Drop drop;
public Producer(Drop drop) {
this.drop = drop;
}
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
Random random = new Random();
for (int i = 0;
i < importantInfo.length;
i++) {
drop.put(importantInfo[i]);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {
}
}
drop.put("DONE");
}
}
The consumer thread defined in Consumer just retrieves the messages and prints them out until it retrieves the "DONE" string. This thread is also paused at random intervals.
import java.util.Random;
public class Consumer implements Runnable {
private Drop drop;
public Consumer(Drop drop) {
this.drop = drop;
}
public void run() {
Random random = new Random();
for (String message = drop.take();
! message.equals("DONE");
message = drop.take()) {
System.out.format("MESSAGE RECEIVED: %s%n", message);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {
}
}
}
}
Finally, this is the main thread defined in ProducerConsumerExample , which starts the producer and consumer threads.
public class ProducerConsumerExample {
public static void main(String[] args) {
Drop drop = new Drop();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
Note:
Drop
The class was written to demonstrate protected blocks. To avoid reinventing the wheel, check the existing data structures in the Java Collections Framework before attempting to write your own data-sharing objects . See the Questions and Exercises section for more information.
6. Immutable objects
An object is considered immutable ( immutable
) if its state cannot be changed after construction . Maximum reliance on immutable objects is widely accepted as a solid strategy for creating simple, reliable code.
Immutable objects are especially useful in concurrent applications. Because they cannot change state, they cannot be corrupted by thread interference and cannot be left in an inconsistent state.
Programmers are often reluctant to use immutable objects because they worry about the cost of creating new objects instead of updating objects in place. The impact of object creation is often overestimated, and is offset by some of the efficiencies associated with immutable objects. This includes reducing the overhead due to garbage collection, and eliminating the code needed to protect mutable objects from corruption.
The subsections below take a class whose instances are mutable, and derive a class from it that has immutable instances. In doing so, they give general rules for such conversions and demonstrate some of the advantages of immutable objects.
6.1 An example of a synchronization class
The SynchronizedRGB class defines objects that represent colors. Each object represents a color as three integers (representing the primary color value) and a string (representing the color name).
public class SynchronizedRGB {
// Values must be between 0 and 255.
private int red;
private int green;
private int blue;
private String name;
private void check(int red,
int green,
int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public SynchronizedRGB(int red,
int green,
int blue,
String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public void set(int red,
int green,
int blue,
String name) {
check(red, green, blue);
synchronized (this) {
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
}
public synchronized int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public synchronized String getName() {
return name;
}
public synchronized void invert() {
red = 255 - red;
green = 255 - green;
blue = 255 - blue;
name = "Inverse of " + name;
}
}
Must be used with care SynchronizedRGB
to avoid being seen in an inconsistent state. For example, suppose a thread executes the following code:
SynchronizedRGB color =
new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB(); //Statement 1
String myColorName = color.getName(); //Statement 2
If another thread calls color.set
after statement 1, but before statement 2, myColorInt
the value of 2 will not match myColorName
the value of . To avoid this outcome, the two statements must be bound together:
synchronized (color) {
int myColorInt = color.getRGB();
String myColorName = color.getName();
}
This inconsistency is only possible with mutable objects - SynchronizedRGB
it wouldn't be a problem for the immutable version of
6.2 Strategies for defining immutable objects
The following rules define a simple strategy for creating immutable objects. Not all classes that are documented as "immutable" follow these rules . This doesn't necessarily mean that the creators of these classes were sloppy -- they probably had good reason to believe that instances of the class would never change after construction. However, this strategy requires complex analysis and is not suitable for beginners.
- Do not provide "setter" methods -- methods that modify the field or the object referenced by the field.
- Set all fields to
final
andprivate
. - Do not allow subclasses to override methods. The easiest way is to declare the class as
final
. A more complicated approach would be to have a constructorprivate
and construct the instance in a factory method. - If instance fields contain references to mutable objects, these objects are not allowed to be changed:
1) Do not provide methods to modify mutable objects.
2) Don't share references to mutable objects. Never store a reference to an external mutable object passed to a constructor; if necessary, make a copy, and store a reference to the copy. Similarly, create copies of internal mutable objects when necessary to avoid returning the original object in methods.
Applying this policy SynchronizedRGB
will result in the following steps:
- There are two setter methods in this class. The first is
set
that it transforms the object arbitrarily, which has no place in the immutable version of the class. The second isinvert
that it can be adjusted by creating a new object rather than modifying an existing one. - All fields are
private
; they are further decorated withfinal
. - The class itself is declared as
final
. - There is only one field pointing to an object, and the object itself is immutable. Therefore, no modification of the state of the "contained" mutable object is required.
After these changes we have ImmutableRGB :
final public class ImmutableRGB {
// Values must be between 0 and 255.
final private int red;
final private int green;
final private int blue;
final private String name;
private void check(int red,
int green,
int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public ImmutableRGB(int red,
int green,
int blue,
String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public String getName() {
return name;
}
public ImmutableRGB invert() {
return new ImmutableRGB(255 - red,
255 - green,
255 - blue,
"Inverse of " + name);
}
}
7. Higher-order concurrent objects
7.1 Lock Objects
Synchronized code relies on a simple reentrant lock ( reentrant lock
). This lock is convenient to use, but has many limitations. java.util.concurrent.locks
Package supports more complex locking idioms. We won't examine this package in detail, but will focus on its most basic interface, Lock .
Lock
Objects work much like the implicit locks used by synchronized code. As with implicit locks, only one thread can own an Lock
object at a time. Objects also support mechanisms Lock
through their associated Conditionwait/notify
objects .
Lock
The biggest advantage objects have over implicit locks is their ability to exit attempts to acquire locks. tryLock
The method will exit if the lock is not immediately available or before the timeout expires (if specified) . If another thread sends an interrupt before acquiring the lock, lockInterruptibly
the method exits .
Let's use Lock
objects to solve the deadlock problem we saw in Liveness . Alphonse and Gaston train themselves to notice their friends bowing. We model this improvement by requiring Friend
the object to acquire locks for both participants before continuing execution . bow
Below is Safelock
the source code of the improved model. To demonstrate the versatility of this idiom, let's assume that Alphonse and Gaston are so enamored with their newfound ability to bow safely that they can't stop bowing to each other:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
public class Safelock {
static class Friend {
private final String name;
private final Lock lock = new ReentrantLock();
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public boolean impendingBow(Friend bower) {
Boolean myLock = false;
Boolean yourLock = false;
try {
myLock = lock.tryLock();
yourLock = bower.lock.tryLock();
} finally {
if (! (myLock && yourLock)) {
if (myLock) {
lock.unlock();
}
if (yourLock) {
bower.lock.unlock();
}
}
}
return myLock && yourLock;
}
public void bow(Friend bower) {
if (impendingBow(bower)) {
try {
System.out.format("%s: %s has"
+ " bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
} finally {
lock.unlock();
bower.lock.unlock();
}
} else {
System.out.format("%s: %s started"
+ " to bow to me, but saw that"
+ " I was already bowing to"
+ " him.%n",
this.name, bower.getName());
}
}
public void bowBack(Friend bower) {
System.out.format("%s: %s has" +
" bowed back to me!%n",
this.name, bower.getName());
}
}
static class BowLoop implements Runnable {
private Friend bower;
private Friend bowee;
public BowLoop(Friend bower, Friend bowee) {
this.bower = bower;
this.bowee = bowee;
}
public void run() {
Random random = new Random();
for (;;) {
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
}
bowee.bow(bower);
}
}
}
public static void main(String[] args) {
final Friend alphonse =
new Friend("Alphonse");
final Friend gaston =
new Friend("Gaston");
new Thread(new BowLoop(alphonse, gaston)).start();
new Thread(new BowLoop(gaston, alphonse)).start();
}
}
Lock interface
// 如果锁可用,获取锁并立即返回值true。如果锁不可用,则此方法
// 将立即返回值false。
// 这个方法的典型用法参见文档
boolean tryLock();
7.2 Executors
In all of the previous examples, there is an affinity between Runnable
the task that the new thread (defined by its object) is performing and the thread itself (defined by the object). Thread
This works well for small applications, but in larger applications it makes sense to separate thread management and creation from the rest of the application . Objects that encapsulate these functions are called executors. The following subsections describe the actuators in detail.
- Executor Interfaces defines three executor object types.
- The most common type of executor implementation for Thread Pools .
- Fork/Join is a framework for utilizing multiple processors (new in JDK 7).
7.2.1 Actuator interface
java.util.concurrent
The package defines three actuator interfaces:
Executor
: A simple interface that supports starting new tasks.ExecutorService
:Executor
a subinterface that adds features that help manage lifecycles, both for individual tasks andExecutor
themselves.ScheduledExecutorService
:ExecutorService
subinterface that supports future and/or periodic execution of tasks.
Executor interface
The Executor interface provides a method execute
designed as a drop-in replacement for the common thread creation idiom. If r
it is an Runnable
object, e
but an object of an Executor
object, you can e.execute(r);
replace it with(new Thread(r)).start();
However, execute
the definition of is less specific. Low-level usage creates a new thread and starts it immediately. Depending on Executor
the implementation, execute may do the same thing, but is more likely to use an existing worker thread to run r
, or will be r
placed in a queue waiting for a worker thread to become available. (We'll describe worker threads in the thread pool section.)
java.util.concurrent
The executor implementations in are designed to take full advantage of the higher-level ExecutorService
and ScheduledExecutorService
interfaces, although they also use the basic Executor
ones.
ExecutorService interface
The ExecutorServicesubmit
interface complements execute
the method with a similar but more general method. As with execute
, submit
also accepts Runnable
objects , but also accepts Callable objects, which allow a task to return a value. submit
The method returns a Future object, which is used to retrieve Callable
the return value and manage Callable
the Runnable
state of the task.
ExecutorService
Methods are also provided for submitting large Callable
collections of objects. Finally, ExecutorService
a number of methods are provided to manage shutdown of executors. To support immediate shutdown, tasks should handle interrupts properly .
ScheduledExecutorService interface
The ScheduledExecutorService interface schedule
is used to supplement its parent ExecutorService
's methods to execute or tasks schedule
after a specified delayRunnable
Callable
. In addition, the interface defines scheduleAtFixedRate
and scheduleWithFixedDelay
, which repeatedly execute the specified task at defined intervals .
7.2.2 Thread pool
java.util.concurrent
Most executor implementations in use a thread pool ( thread pools
) consisting of worker threads ( worker threads
). This type of thread exists independently of the task it executes, and is often used to perform multiple Runnable
tasks .Callable
Using worker threads minimizes the overhead introduced by thread creation . Thread objects use a lot of memory, and in large-scale applications, allocating and deallocating many thread objects can incur significant memory management overhead.
A common type of thread pool is the fixed thread pool ( fixed thread pool
). This type of pool always has a specified number of threads running; if a thread somehow terminates while it is still in use, it is automatically replaced with a new thread. Tasks are submitted to the pool via an internal queue that holds additional tasks whenever there are more active tasks than threads.
An important advantage of a fixed thread pool is that applications using it can degrade nicely ( degrade gracefully
). To understand this, consider a web server application where each HTTP request is handled by a separate thread. If the application simply creates a new thread for each new HTTP request, and the system receives more requests than it can handle at once, the application will suddenly stop responding when the overhead of all those threads exceeds the capacity of the system All requests. Due to the limit on the number of threads that can be created, the application will not be able to serve HTTP requests as fast as they arrive, but it will serve them as fast as the system can handle.
An easy way to create an executor that uses a fixed thread pool is to call the newFixedThreadPool factory method in java.util.concurrent.Executors . This class also provides the following factory methods:
- The newCachedThreadPool method creates an Executor with an expandable thread pool . This executor is suitable for applications that start many short-lived tasks.
- The newsinglethreadeexecutor method creates an executor that executes tasks one at a time.
- There are several factory methods that are versions of the above executors
ScheduledExecutorService
.
If none of the executors provided by the above factory methods meet your needs, then constructing an instance of java.util.concurrent.ThreadPoolExecutor or java.util.concurrent.ScheduledThreadPoolExecutor will give you additional options.
newFixedThreadPool
// 返回 新创建的线程池
public static ExecutorService newFixedThreadPool(int nThreads)
7.2.3 Fork/Join
fork/join
A frame is ExecutorService
an implementation of an interface that helps you take advantage of multiple processors . 它是为那些可以递归分解成小块的工作而设计的
. The goal is to use all available processing power to enhance the performance of the application.
As with any ExecutorService
implementation, fork/join
the framework distributes tasks to worker threads in a thread pool. fork/join
The frame is different because it uses the task-stealing( work-stealing
) algorithm. Worker threads with nothing to do can steal tasks from other threads that are still busy.
fork/join
The center of the framework is the ForkJoinPool class, which is AbstractExecutorService
an extension of the class. ForkJoinPool
The core task-stealing algorithm is implemented, and the ForkJoinTask process can be executed.
basic use
The first step in using fork/join
a framework is to write code that performs some of the work. Your code should resemble the following pseudocode:
if (my portion of the work is small enough)
do the work directly
else
split my work into two pieces
invoke the two pieces and wait for the results
Wrap this code in ForkJoinTask
a subclass, usually using one of its more specialized types, RecursiveTask (which can return results) or RecursiveAction .
After ForkJoinTask
the subclass is ready, create an object representing all the work to be done and pass it to ForkJoinPool
the instance's invoke()
methods.
blur for clarity
To help you understand fork/join
how the framework works, consider the following example. Say you want to blur an image. The original source image is represented by an array of integers, where each integer contains the color value of a single pixel. The blurred destination image is also represented by an integer array of the same size as the source image.
Performing blurring is done by processing the source array one pixel at a time. Each pixel is averaged with its surrounding pixels (red, green, blue component averaged) and the result is placed in the destination array. Since the image is a large array, this process can take a long time. By implementing the algorithm using the fork/join framework, you can take advantage of concurrent processing on multiprocessor systems. Here's a possible implementation:
public class ForkBlur extends RecursiveAction {
private int[] mSource;
private int mStart;
private int mLength;
private int[] mDestination;
// Processing window size; should be odd.
private int mBlurWidth = 15;
public ForkBlur(int[] src, int start, int length, int[] dst) {
mSource = src;
mStart = start;
mLength = length;
mDestination = dst;
}
protected void computeDirectly() {
int sidePixels = (mBlurWidth - 1) / 2;
for (int index = mStart; index < mStart + mLength; index++) {
// Calculate average.
float rt = 0, gt = 0, bt = 0;
for (int mi = -sidePixels; mi <= sidePixels; mi++) {
int mindex = Math.min(Math.max(mi + index, 0),
mSource.length - 1);
int pixel = mSource[mindex];
rt += (float)((pixel & 0x00ff0000) >> 16)
/ mBlurWidth;
gt += (float)((pixel & 0x0000ff00) >> 8)
/ mBlurWidth;
bt += (float)((pixel & 0x000000ff) >> 0)
/ mBlurWidth;
}
// Reassemble destination pixel.
int dpixel = (0xff000000 ) |
(((int)rt) << 16) |
(((int)gt) << 8) |
(((int)bt) << 0);
mDestination[index] = dpixel;
}
}
...
Now implement the abstract compute()
method that either performs the blurring directly or splits it into two smaller tasks. A simple array length threshold helps determine whether to perform work or split it.
protected static int sThreshold = 100000;
protected void compute() {
if (mLength < sThreshold) {
computeDirectly();
return;
}
int split = mLength / 2;
invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
new ForkBlur(mSource, mStart + split, mLength - split,
mDestination));
}
If the previous method is in RecursiveAction
a subclass of the class, then setting up the task ForkJoinPool
to run in is straightforward and involves the following steps:
- Create a task that represents all the work to be done.
// source image pixels are in src
// destination image pixels are in dst
ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
- Create the one that will run the task
ForkJoinPool
.
ForkJoinPool pool = new ForkJoinPool();
- run task
pool.invoke(fb);
For the complete source code, including some extra code to create the target image file, see the ForkBlur sample.
standard implementation
In addition to using fork/join
frameworks to implement custom algorithms for tasks that execute concurrently on multiprocessor systems (as in the example in the previous section ForkBlur.java
), there are some generally useful features in Java SE that have been implemented using fork/join frameworks. One such implementation was introduced in Java SE 8, used by the java.util.Arrays class for its parallelSort()
methods. These methods are similar , but leverage concurrency sort()
through the framework. fork/join
Parallel sorting of large arrays is faster than sequential sorting when running on multiprocessor systems. However, exactly how these methods utilize fork/join
the framework is beyond the scope of the Java Tutorial. See the Java API documentation for these information.
fork/join
Another implementation of the framework is used by methods in the java.util.streams package, which is part of Project Lambda , scheduled for release in Java SE 8 . See the Lambda expressions section for more information.
7.3 Concurrent collections
java.util.concurrent
Package contains many additions to the Java Collections Framework. They are most easily categorized via the provided collection interface:
- BlockingQueue defines a first-in-first-out data structure that blocks or times out when you try to add data to a full queue or retrieve data from an empty queue.
- ConcurrentMap is a subinterface of java.util.Map that defines useful atomic operations. These operations delete or replace key-value pairs only if the key exists, or add key-value pairs only if the key does not exist . Making these operations atomic helps avoid synchronization.
ConcurrentMap
The standard generic implementation of is ConcurrentHashMap , which is a concurrent analog of HashMap . - ConcurrentNavigableMap is
ConcurrentMap
a subinterface of ConcurrentNavigableMap that supports approximate matching .ConcurrentNavigableMap
The standard generic implementation of is ConcurrentSkipListMap , which is a concurrent analog of TreeMap .
All of these collections help avoid memory consistency errors by defining a happens-before relationship between operations that add an object to the collection and subsequent operations that access or delete that object .
7.4 Atomic variables
The java.util.concurrent.atomic package defines classes that support atomic operations on a single variable . All classes have get
and set
methods, like reading and volatile
writing variables. That is, there is a happens-before relationship set
with any successor on the same variable . get
Atomic compareAndSet
methods also have these memory consistency properties, as do simple atomic arithmetic methods applied to integer atomic variables.
To see how to use this package, let's go back to the Counter class that was originally used to demonstrate thread interference:
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
One way to protect Counter
against thread interference is to make its methods synchronized, like SynchronizedCounter :
class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
For this simple class, synchronization is an acceptable solution. But for more complex classes, we may want to avoid unnecessary synchronization impact on liveness. Replacing the int field with something like AtomicCounterAtomicInteger
allows us to prevent thread interference without resorting to synchronization :
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private AtomicInteger c = new AtomicInteger(0);
public void increment() {
c.incrementAndGet();
}
public void decrement() {
c.decrementAndGet();
}
public int value() {
return c.get();
}
}
AtomicInteger
// 获取当前值。
public final int get()
// 设置为给定的值。
public final void set(int newValue)
// 原子地设置为给定值并返回旧值。
public final int getAndSet(int newValue)
// 如果当前值==期望值,则自动将值设置为给定的更新值。
// 如果成功,则为 true;False表示实际值不等于期望值。
public final boolean compareAndSet(int expect, int update)
// 将当前值自动加1,返回更新后的值
public final int incrementAndGet()
// 将当前值自动减1,返回:更新后的值
public final int decrementAndGet()
// 自动将给定值添加到当前值,返回:更新后的值
public final int addAndGet(int delta)
// 将当前值自动加1,返回前值
public final int getAndIncrement()
// 将当前值自动减1。返回前值
public final int getAndDecrement()
// 自动将给定值添加到当前值, 返回前值
public final int getAndAdd(int delta)
7.5 Concurrent random numbers
In JDK 7, java.util.concurrent includes a convenience class ThreadLocalRandomForkJoinTasks
for applications that expect to use random numbers from multiple threads or .
For concurrent access, using ThreadLocalRandom
instead Math.random()
can reduce contention and ultimately lead to better performance.
All you need to do is call ThreadLocalRandom.current()
and then call one of its methods to retrieve a random number. Here's an example:
int r = ThreadLocalRandom.current() .nextInt(4, 77);
ThreadLocalRandom
// 返回一个介于指定的起点(origin, 包括)和指定的边界(bound, 不包括)之间的
// 伪随机int值。
public int nextInt(int origin, int bound)
// 返回介于0(包含)和指定的边界(不包含)之间的伪随机int值。
public int nextInt(int bound)
// 返回一个伪随机int值。
public int nextInt()
further references
- Concurrent Programming in Java: Design Principles and Patterns (Second Edition) (
Concurrent Programming in Java: Design Principles and Pattern (2nd Edition)
), by Doug Lea. This is the comprehensive work of a leading expert and architect of the Java Platform Concurrency Framework. - Java Concurrency in Practice (
Java Concurrency in Practice
) by Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, and Doug Lea. A practical guide designed for beginners. - Effective Java Programming Language Guide (Effective Java Programming Language Guide, Second Edition), by Joshua Bloch. Although this is a general programming guide, its chapter on threads covers basic "best practices" for concurrent programming.
- Concurrency: State Models & Java Programs (2nd Edition), by Jeff Magee and Jeff Kramer. Introduces concurrent programming through a combination of modeling and practical examples.
- Java ConcurrencyAnimation (
Java Concurrent Animated
): Animation showing usage of concurrency features.