Chapter 9. Threads

We take for granted that modern computer systems can manage many applications and operating system (OS) tasks running concurrently and make it appear that all the software is running simultaneously. Most systems today have multiple processors and or at least multiple cores and can achieve an impressive degree of parallelism. The operating system still juggles applications at a higher level but turns its attention from one to the next so quickly that they also appear to run at once.

In the old days, the unit of concurrency for such systems was the application or process. To the OS, a process was more or less a black box that decided what to do on its own. If an application required greater concurrency, it could get it only by running multiple processes and communicating between them, but this was a heavyweight approach and not very elegant. Later, the concept of threads was introduced. Threads provide fine-grained concurrency within a process under the application’s own control. Threads have existed for a long time, but have historically been tricky to use. In Java, support for threading is built into the language, making it easier to work with threads. The Java concurrency utilities address common patterns and practices in multithreaded applications and raise them to the level of tangible Java APIs. Collectively, this means that Java is a language that supports threading both natively and at a high level. It also means that Java’s APIs take full advantage of threading, so it’s important that you gain some degree of familiarity with these concepts early in your exploration of Java. Not all developers will need to write applications that explicitly use threads or concurrency, but most will use some feature that is impacted by them.

Threads are integral to the design of many Java APIs, especially those involved in client-side applications, graphics, and sound. For example, when we look at GUI programming later in this book, you’ll see that a component’s paint() method isn’t called directly by the application but rather by a separate drawing thread within the Java runtime system. At any given time, many such background threads may be performing activities in parallel with your application. On the server side, Java threads are there as well, servicing every request and running your application components. It’s important to understand how your code fits into that environment.

In this chapter, we’ll talk about writing applications that create and use their own threads explicitly. We’ll talk about the low-level thread support built into the Java language first and then discuss the java.util.concurrent thread utilities package in detail at the end of this chapter.

Introducing Threads

Conceptually, a thread is a flow of control within a program. A thread is similar to the more familiar notion of a process, except that threads within the same application are much more closely related and share much of the same state. It’s kind of like a golf course, which many golfers use at the same time. The threads cooperate to share a working area. They have access to the same objects, including static and instance variables, within their application. However, threads have their own copies of local variables, just as players share the golf course but do not share some personal items like clubs and balls.

Multiple threads in an application have the same problems as the golfers—in a word, synchronization. Just as you can’t have two sets of players blindly playing the same green at the same time, you can’t have several threads trying to access the same variables without some kind of coordination. Someone is bound to get hurt. A thread can reserve the right to use an object until it’s finished with its task, just as a golf party gets exclusive rights to the green until it’s done. And a thread that is more important can raise its priority, asserting its right to play through.

The devil is in the details, of course, and those details have historically made threads difficult to use. Fortunately, Java makes creating, controlling, and coordinating threads simpler by integrating some of these concepts directly into the language.

It is common to stumble over threads when you first work with them because creating a thread exercises many of your new Java skills all at once. You can avoid confusion by remembering that two players are always involved in running a thread: a Java language Thread object that represents the thread itself and an arbitrary target object that contains the method that the thread is to execute. Later, you will see that it is possible to play some sleight of hand and combine these two roles, but that special case just changes the packaging, not the relationship.

The Thread Class and the Runnable Interface

All execution in Java is associated with a Thread object, beginning with a “main” thread that is started by the Java VM to launch your application. A new thread is born when we create an instance of the java.lang.Thread class. The Thread object represents a real thread in the Java interpreter and serves as a handle for controlling and coordinating its execution. With it, we can start the thread, wait for it to complete, cause it to sleep for a time, or interrupt its activity. The constructor for the Thread class accepts information about where the thread should begin its execution. Conceptually, we would like to simply tell it what method to run. There are a number of ways to do this; Java 8 allows method references that would do the trick. Here we will take a short detour and use the java.lang.Runnable interface to create or mark an object that contains a “runnable” method. Runnable defines a single, general-purpose run() method:

    public interface Runnable {
         abstract public void run();
    }

Every thread begins its life by executing the run() method in a Runnable object, which is the “target object” that was passed to the thread’s constructor. The run() method can contain any code, but it must be public, take no arguments, have no return value, and throw no checked exceptions.

Any class that contains an appropriate run() method can declare that it implements the Runnable interface. An instance of this class is then a runnable object that can serve as the target of a new thread. If you don’t want to put the run() method directly in your object (and very often you don’t), you can always make an adapter class that serves as the Runnable for you. The adapter’s run() method can then call any method it wants after the thread is started. We’ll show examples of these options later.

Creating and starting threads

A newly born thread remains idle until we give it a figurative slap on the bottom by calling its start() method. The thread then wakes up and proceeds to execute the run() method of its target object. start() can be called only once in the lifetime of a thread. Once a thread starts, it continues running until the target object’s run() method returns (or throws an unchecked exception of some kind). The start() method has a sort of evil twin method called stop(), which kills the thread permanently. However, this method is deprecated and should no longer be used. We’ll explain why and give some examples of a better way to stop your threads later in this chapter. We will also look at some other methods you can use to control a thread’s progress while it is running.

Let’s look at an example. The following class, Animator, implements a run() method to drive a drawing loop we could use in our game for updating the Field:

    class Animator implements Runnable {
        boolean animating = true;

        public void run() {
            while ( animating ) {
                // move apples one "frame"
                // repaint field
                // pause
                ...
            }
        }
    }

To use it, we create a Thread object, passing it an instance of Animator as its target object, and invoke its start() method. We can perform these steps explicitly:

    Animator myAnimator = new Animator();
    Thread myThread = new Thread(myAnimator);
    myThread.start();
lj5e 0901
Figure 9-1. Animator as an implementation of Runnable

We created an instance of our Animator class and passed it as the argument to the constructor for myThread. As shown in Figure 9-1, when we call the start() method, myThread begins to execute Animator’s run() method. Let the show begin!

A natural-born thread

The Runnable interface lets us make an arbitrary object the target of a thread, as we did in the previous example. This is the most important general usage of the Thread class. In most situations in which you need to use threads, you’ll create a class (possibly a simple adapter class) that implements the Runnable interface.

However, we’d be remiss not to show you the other technique for creating a thread. Another design option is to make our target class a subclass of a type that is already runnable. As it turns out, the Thread class itself conveniently implements the Runnable interface; it has its own run() method, which we can override directly to do our bidding:

    class Animator extends Thread {
        boolean animating = true;

        public void run() {
            while ( animating ) {
                // draw Frames
                ...
            }
        }
    }

The skeleton of our Animator class looks much the same as before, except that our class is now a subclass of Thread. To go along with this scheme, the default constructor of the Thread class makes itself the default target—that is, by default, the Thread executes its own run() method when we call the start() method, as shown in Figure 9-2. Now our subclass can just override the run() method in the Thread class. (Thread itself defines an empty run() method.)

lj5e 0902
Figure 9-2. Animator as a subclass of Thread

Next, we create an instance of Animator and call its start() method (which it also inherited from Thread):

    Animator bouncy = new Animator();
    bouncy.start();

Alternatively, we can have the Animator object start its thread when it is created:

    class Animator extends Thread {

         Animator () {
             start();
         }
         ...
    }

Here, our Animator object just calls its own start() method when an instance is created. (It’s probably better form to start and stop our objects explicitly after they’re created rather than starting threads as a hidden side effect of object creation, but this serves the example well.)

Subclassing Thread may seem like a convenient way to bundle a thread and its target run() method. However, this approach often isn’t the best design. If you subclass Thread to implement a thread, you are saying you need a new type of object that is a kind of Thread, which exposes all of the public API of the Thread class. While there is something satisfying about taking an object that’s primarily concerned with performing a task and making it a Thread, the actual situations where you’ll want to create a subclass of Thread should not be very common. In most cases, it is more natural to let the requirements of your program dictate the class structure and use Runnables to connect the execution and logic of your program.

Controlling Threads

We have seen the start() method used to begin execution of a new thread. Several other instance methods let us explicitly control a thread’s execution:

  • The static Thread.sleep() method causes the currently executing thread to wait for a designated period of time (give or take), without consuming much (or possibly any) CPU time.

  • The methods wait() and join() coordinate the execution of two or more threads. We’ll discuss them in detail when we talk about thread synchronization later in this chapter.

  • The interrupt() method wakes up a thread that is sleeping in a sleep() or wait() operation or is otherwise blocked on a long I/O operation.1

Deprecated methods

We should also mention three deprecated thread control methods: stop(), suspend(), and resume(). The stop() method complements start(); it destroys the thread. start() and the deprecated stop() method can be called only once in the thread’s lifecycle. By contrast, the deprecated suspend() and resume() methods were used to arbitrarily pause and then restart the execution of a thread.

Although these deprecated methods still exist in the latest version of Java (and will probably be there forever), they shouldn’t be used in new code development. The problem with both stop() and suspend() is that they seize control of a thread’s execution in an uncoordinated, harsh way. This makes programming difficult; it’s not always easy for an application to anticipate and properly recover from being interrupted at an arbitrary point in its execution. Moreover, when a thread is seized using one of these methods, the Java runtime system must release all its internal locks used for thread synchronization. This can cause unexpected behavior and, in the case of suspend() which does not release these locks, can easily lead to deadlock.

A better way to affect the execution of a thread—which requires just a bit more work on your part—is by creating some simple logic in your thread’s code to use monitor variables (if these variables are boolean, you might see them referred to as “flags”), possibly in conjunction with the interrupt() method, which allows you to wake up a sleeping thread. In other words, you should cause your thread to stop or resume what it is doing by asking it nicely rather than by pulling the rug out from under it unexpectedly. The thread examples in this book use this technique in one way or another.

The sleep() method

We often need to tell a thread to sit idle, or “sleep,” for a fixed period of time. While a thread is asleep, or otherwise blocked from input of some kind, it doesn’t consume CPU time or compete with other threads for processing. For this, we can call the static method Thread.sleep(), which affects the currently executing thread. The call causes the thread to go idle for a specified number of milliseconds:

    try {
        // The current thread
        Thread.sleep( 1000 );
    } catch ( InterruptedException e ) {
        // someone woke us up prematurely
    }

The sleep() method may throw an InterruptedException if it is interrupted by another thread via the interrupt() method (more below). As you see in the previous code, the thread can catch this exception and take the opportunity to perform some action—such as checking a variable to determine whether or not it should exit—or perhaps just perform some housekeeping and then go back to sleep.

The join() method

Finally, if you need to coordinate your activities with another thread by waiting for it to complete its task, you can use the join() method. Calling a thread’s join() method causes the caller to block until the target thread completes. Alternatively, you can poll the thread by calling join() with a number of milliseconds to wait. This is a very coarse form of thread synchronization. Java supports more general and powerful mechanisms for coordinating thread activity including the wait() and notify() methods, as well as higher-level APIs in the java.util.concurrent package. We have to leave those topics mostly for your own exploration, but it’s worth pointing out that the Java language makes multithreaded code easier to write than many of its predecessors.

The interrupt() method

Earlier, we described the interrupt() method as a way to wake up a thread that is idle in a sleep(), wait(), or lengthy I/O operation. Any thread that is not running continuously (not a “hard loop”) must enter one of these states periodically and so this is intended to be a point where the thread can be flagged to stop. When a thread is interrupted, its interrupt status flag is set. This can happen at any time, whether the thread is idle or not. The thread can test this status with the isInterrupted() method. isInterrupted(boolean), another form, accepts a Boolean value indicating whether or not to clear the interrupt status. In this way, a thread can use the interrupt status as a flag and a signal.

This is indeed the prescribed functionality of the method. However, historically, this has been a weak spot, and Java implementations have had trouble getting it to work correctly in all cases. In the earliest Java VMs (prior to version 1.1), interrupt() did not work at all. More recent versions still have problems with interrupting I/O calls. By an I/O call, we mean when an application is blocked in a read() or write() method, moving bytes to or from a source such as a file or the network. In this case, Java is supposed to throw an InterruptedIOException when the interrupt() is performed. However, this has never been reliable across all Java implementations. The New I/O framework (java.nio) was introduced all the way back in Java 1.4 with one of its goals being to specifically address these problems. When the thread associated with an NIO operation is interrupted, the thread wakes up and the I/O stream (called a “channel”) is automatically closed. (See Chapter 11 for more about the NIO package.)

Revisiting animation with threads

As we discussed at the beginning of this chapter, a common task in graphical interfaces is managing animations. Sometimes the animations are subtle transitions, other times they are the focus of the application itself as with our apple tossing game. There are a number of ways to implement animation; we’ll look at using simple threads alongside the sleep() functions as well as using a timer. Pairing those options with some type of stepping or “next frame” function is a popular approach that is also easy to understand. We’ll show both techniques to animate our flying apples.

We can use a thread similar to “Creating and starting threads” to produce real animation. The basic idea is to paint or position all of your animated objects, pause, move them to their next spots, and then repeat. Let’s take a look at how we draw some pieces of our game field without animation first.

// From the Field class...
    protected void paintComponent(Graphics g) {
        g.setColor(fieldColor);
        g.fillRect(0,0, getWidth(), getHeight());
        physicist.draw(g);
        for (Tree t : trees) {
            t.draw(g);
        }
        for (Apple a : apples) {
            a.draw(g);
        }
    }

// And from the Apple class...
    public void draw(Graphics g) {
        // Make sure our apple will be red, then paint it!
        g.setColor(Color.RED);
        g.fillOval(x, y, scaledLength, scaledLength);
    }

Easy enough. We start by painting the background field, then our phsycist, then the trees, and finally any apples. That guarantees the apples will show “on top” of the other elements. The Field class overrides the mid-level paintComponent() method available to all graphical elements in Java’s Swing for custom drawing, but more on that in Chapter 10.

Now if we think about what changes on the screen as we play, there are really two “moveable” items: the apple our physicist is aiming from their tower, and any apples actively flying after being tossed. We know the aiming “animation” is just in response to updating the physicist as we move a slider. That doesn’t require separate animation. So we only need to concentrate on handling flying apples. That means our game’s step function should move every apple that is active according to the rules of gravity. Here are the two methods that cover this work. We set up the initial conditions in the toss() method according to the values of our physicist’s aiming and force sliders. Then we make one move for the apple in the step() method.

// From the Apple class...

    public void toss(float angle, float velocity) {
        lastStep = System.currentTimeMillis();
        double radians = angle / 180 * Math.PI;
        velocityX = (float)(velocity * Math.cos(radians) / mass);
        // Start with negative velocity since "up" means smaller values of y
        velocityY = (float)(-velocity * Math.sin(radians) / mass);
    }

    public void step() {
        // Make sure we're moving at all using our lastStep tracker as a sentinel
        if (lastStep > 0) {
            // let's apply our gravity
            long now = System.currentTimeMillis();
            float slice = (now - lastStep) / 1000.0f;
            velocityY = velocityY + (slice * Field.GRAVITY);
            int newX = (int)(centerX + velocityX);
            int newY = (int)(centerY + velocityY);
            setPosition(newX, newY);
        }
    }

Now that we know how to update our apples, we can put that in an animation loop that will do the update calculations, repaint our field, pause, and repeat.

public static final int STEP = 40;   // duration of an animation frame in milliseconds

// ...

class Animator implements Runnable {
    public void run() {
        // "animating" is a global variable that allows us to stop animating
        // and conserve resources if there are no active apples to move
        while (animating) {
            System.out.println("Stepping " + apples.size() + " apples");
            for (Apple a : apples) {
                a.step();
                detectCollisions(a);
            }
            Field.this.repaint();
            cullFallenApples();
            try {
                Thread.sleep((int)(STEP * 1000));
            } catch (InterruptedException ie) {
                System.err.println("Animation interrupted");
                animating = false;
            }
        }
    }
}

We’ll use this implementation of Runnable in a simple thread. Our Field class will keep an instance of the thread around and contains the following simple start method:

    Thread animationThread;

    // ...

    void startAnimation() {
        animationThread = new Thread(new Animator());
        animationThread.start();
    }

With the UI events we’ll be discussing in “Events”, we could launch our apples on command. For now, we’ll just launch the first apple as soon as our game starts. We know Figure 9-3 doesn’t look like much as a still screenshot, but trust us, it is amazing in person. :)

lj5e 0903
Figure 9-3. Tossable apples in action

Death of a Thread

A thread continues to execute until one of the following happens:

  • It explicitly returns from its target run() method.

  • It encounters an uncaught runtime exception.

  • The evil and nasty deprecated stop() method is called.

What happens if none of these things occurs, and the run() method for a thread never terminates? The answer is that the thread can live on, even after what is ostensibly the part of the application that created it has finished. This means we have to be aware of how our threads eventually terminate, or an application can end up leaving orphaned threads that unnecessarily consume resources or keep the application alive when it would otherwise quit.

In many cases, we really want to create background threads that do simple, periodic tasks in an application. The setDaemon() method can be used to mark a thread as a daemon thread that should be killed and discarded when no other nondaemon application threads remain. Normally, the Java interpreter continues to run until all threads have completed. But when daemon threads are the only threads still alive, the interpreter will exit.

Here’s a devilish example using daemon threads:

    class Devil extends Thread {
        Devil() {
            setDaemon( true );
            start();
        }
        public void run() {
            // perform evil tasks
        }
    }

In this example, the Devil thread sets its daemon status when it is created. If any Devil threads remain when our application is otherwise complete, the runtime system kills them for us. We don’t have to worry about cleaning them up.

Daemon threads are primarily useful in standalone Java applications and in the implementation of server frameworks, but not in component applications (where a small piece of code runs inside a larger one). Since these components run inside another Java application, any daemon threads they might create can continue to live until the controlling application exits—probably not the desired effect. Any such application can use ThreadGroups to contain all the threads created by subsystems or components and then clean them up if necessary.

One final note about killing threads gracefully. A very common problem new developers encounter the first time they create an application using a Swing component is that their application never exits; the Java VM seems to hang indefinitely after everything is finished. When working with graphics, Java has created a UI thread to process input and painting events. The UI thread is not a daemon thread, so it doesn’t exit automatically when other application threads have completed, and the developer must call System.exit() explicitly. (If you think about it, this makes sense. Because most GUI applications are event-driven and simply wait for user input, they would otherwise simply exit after their startup code completed.)

Synchronization

Every thread has a mind of its own. Normally, a thread goes about its business without any regard for what other threads in the application are doing. Threads may be time-sliced, which means they can run in arbitrary spurts and bursts as directed by the operating system. On a multiprocessor or multicore system, it is even possible for many different threads to be running simultaneously on different CPUs. This section is about coordinating the activities of two or more threads so that they can work together and not collide in their use of the same variables and methods (coordinating their play on the golf course).

Java provides a few simple structures for synchronizing the activities of threads. They are all based on the concept of monitors, a widely used synchronization scheme. You don’t have to know the details about how monitors work to be able to use them, but it may help you to have a picture in mind.

A monitor is essentially a lock. The lock is attached to a resource that many threads may need to access, but that should be accessed by only one thread at a time. It’s very much like a restroom with a lock on the door; if it’s unlocked, you can enter and lock the door while you are using it. If the resource is not being used, the thread can acquire the lock and access the resource. When the thread is done, it relinquishes the lock, just as you unlock the restroom door and leave it open for the next person. However, if another thread already has the lock for the resource, all other threads must wait until the current thread is done and has released the lock. This is just like when the restroom is occupied when you arrive: you have to wait until the current user is done and unlocks the door.

Fortunately, Java makes the process of synchronizing access to resources fairly easy. The language handles setting up and acquiring locks; all you need to do is specify the resources that require synchronization.

Serializing Access to Methods

The most common need for synchronization among threads in Java is to serialize their access to some resource (an object)—in other words, to make sure that only one thread at a time can manipulate an object or variable.2 In Java, every object has an associated lock. To be more specific, every class and every instance of a class has its own lock. The synchronized keyword marks places where a thread must acquire the lock before proceeding.

For example, suppose we implemented a SpeechSynthesizer class that contains a say() method. We don’t want multiple threads calling say() at the same time because we wouldn’t be able to understand anything being said. So we mark the say() method as synchronized, which means that a thread must acquire the lock on the SpeechSynthesizer object before it can speak:

    class SpeechSynthesizer {
        synchronized void say( String words ) {
            // speak
        }
    }

Because say() is an instance method, a thread must acquire the lock on the SpeechSynthesizer instance it’s using before it can invoke the say() method. When say() has completed, it gives up the lock, which allows the next waiting thread to acquire the lock and run the method. It doesn’t matter whether the thread is owned by the SpeechSynthesizer itself or some other object; every thread must acquire the same lock, that of the SpeechSynthesizer instance. If say() were a class (static) method instead of an instance method, we could still mark it as synchronized. In this case, because no instance object is involved, the lock is on the class object itself.

Often, you want to synchronize multiple methods of the same class so that only one method modifies or examines parts of the class at a time. All static synchronized methods in a class use the same class object lock. By the same token, all instance methods in a class use the same instance object lock. In this way, Java can guarantee that only one of a set of synchronized methods is running at a time. For example, a SpreadSheet class might contain a number of instance variables that represent cell values as well as some methods that manipulate the cells in a row:

    class SpreadSheet {
        int cellA1, cellA2, cellA3;

        synchronized int sumRow() {
            return cellA1 + cellA2 + cellA3;
        }

        synchronized void setRow( int a1, int a2, int a3 ) {
            cellA1 = a1;
            cellA2 = a2;
            cellA3 = a3;
        }
        ...
    }

In this example, methods setRow() and sumRow() both access the cell values. You can see that problems might arise if one thread were changing the values of the variables in setRow() at the same moment another thread was reading the values in sumRow(). To prevent this, we have marked both methods as synchronized. When threads are synchronized, only one runs at a time. If a thread is in the middle of executing setRow() when another thread calls sumRow(), the second thread waits until the first one finishes executing setRow() before it runs sumRow(). This synchronization allows us to preserve the consistency of the SpreadSheet. The best part is that all this locking and waiting is handled by Java; it’s invisible to the programmer.

In addition to synchronizing entire methods, the synchronized keyword can be used in a special construct to guard arbitrary blocks of code. In this form, it also takes an explicit argument that specifies the object for which it is to acquire a lock:

    synchronized ( myObject ) {
        // Functionality that needs exclusive access to resources
    }

This code block can appear in any method. When it is reached, the thread has to acquire the lock on myObject before proceeding. In this way, we can synchronize methods (or parts of methods) in different classes in the same way as methods in the same class.

A synchronized instance method is, therefore, equivalent to a method with its statements synchronized on the current object. Thus:

    synchronized void myMethod () {
        ...
}

is equivalent to:

    void myMethod () {
        synchronized ( this ) {
            ...
        }
    }

We can demonstrate the basics of synchronization with a classic “producer/consumer” scenario. We have some common resource with producers creating new resources and consumers grabbing and using those resources. An example might be a series of web crawlers picking up images online. The “producer” in this could be a thread (or multiple threads) doing the actual work of loading and parsing web pages looking for images and their URLs. Those URLs could be placed in a common queue and the “consumer” thread(s) would pick up the next URL in the queue and actually download the image to the file system or a database. We won’t try to do all of the real I/O here (more on files and networking in Chapter 11) but we can easily setup some producing and consuming threads to see how the synchronization works.

Synchronizing a queue of URLs

Let’s look first at the queue where the URLs will be stored. We’re not trying to be fancy with the queue itself, it’s just a list that we can append URLs (as Strings) to the end and pull them off from the front. We’ll use a LinkedList similar to the ArrayList we saw in Chapter 7. It is a structure designed for the efficient access and manipulation that we want for this queue.

package ch09;

import java.util.LinkedList;

public class URLQueue {
    LinkedList<String> urlQueue = new LinkedList<>();

    public synchronized void addURL(String url) {
        urlQueue.add(url);
    }

    public synchronized String getURL() {
        if (!urlQueue.isEmpty()) {
            return urlQueue.removeFirst();
        }
        return null;
    }

    public boolean isEmpty() {
        return urlQueue.isEmpty();
    }
}

Note that not every method is synchronized! We allow any thread to ask about whether the queue is empty or not without holding up other threads that might be adding or removing. This does mean that we might report a wrong answer—if the timing of different threads is exactly wrong—but our system is somewhat fault tolerant, so the efficiency of not locking the queue just to check its size wins out over more perfect knowledge.3

Now that we know how we’ll be storing and retrieving our URLs, we can create the producer and consumer classes. The producer will run a simple loop to make up fake URLs, prefix them with a producer ID, and store them in our queue. Here’s the run() method for URLProducer:

    public void run() {
        for (int i = 1; i <= urlCount; i++) {
            String url = "https://some.url/at/path/" + i;
            queue.addURL(producerID + " " + url);
            System.out.println(producerID + " produced " + url);
            try {
                Thread.sleep(delay.nextInt(500));
            } catch (InterruptedException ie) {
                System.err.println("Producer " + producerID + " interrupted. Quitting.");
                break;
            }
        }
    }

The consumer class will actually be quite similar, with the obvious exception of taking URLs out of the queue. It will pull a URL out, prefix it with a consumer ID, and start over until the producers are done producing and the queue is empty.

    public void run() {
        while (keepWorking || !queue.isEmpty()) {
            String url = queue.getURL();
            if (url != null) {
                System.out.println(consumerID + " consumed " + url);
            } else {
                System.out.println(consumerID + " skipped empty queue");
            }
            try {
                Thread.sleep(delay.nextInt(1000));
            } catch (InterruptedException ie) {
                System.err.println("Consumer " + consumerID + " interrupted. Quitting.");
                break;
            }
        }
    }

We can start by running our simulation with very small numbers: two producers, two consumers, and each producer will create only three URLs.

package ch09;

public class URLDemo {
    public static void main(String args[]) {
        URLQueue queue = new URLQueue();
        URLProducer p1 = new URLProducer("P1", 3, queue);
        URLProducer p2 = new URLProducer("P2", 3, queue);
        URLConsumer c1 = new URLConsumer("C1", queue);
        URLConsumer c2 = new URLConsumer("C2", queue);
        System.out.println("Starting...");
        p1.start();
        p2.start();
        c1.start();
        c2.start();
        try {
            // Waiti for the producers to finish creating urls
            p1.join();
            p2.join();
        } catch (InterruptedException ie) {
            System.err.println("Interrupted waiting for producers to finish");
        }
        c1.setKeepWorking(false);
        c2.setKeepWorking(false);
        try {
            // Now wait for the workers to clean out the queue
            c1.join();
            c2.join();
        } catch (InterruptedException ie) {
            System.err.println("Interrupted waiting for consumers to finish");
        }
        System.out.println("Done");
    }
}

And even with these tiny numbers involved, we can still see the effects of using multiple threads to do the work:

Starting...
C1 skipped empty queue
C2 skipped empty queue
P2 produced https://some.url/at/path/1
P1 produced https://some.url/at/path/1
P1 produced https://some.url/at/path/2
P2 produced https://some.url/at/path/2
C2 consumed P2 https://some.url/at/path/1
P2 produced https://some.url/at/path/3
P1 produced https://some.url/at/path/3
C1 consumed P1 https://some.url/at/path/1
C1 consumed P1 https://some.url/at/path/2
C2 consumed P2 https://some.url/at/path/2
C1 consumed P2 https://some.url/at/path/3
C1 consumed P1 https://some.url/at/path/3
Done

Notice that the threads don’t take perfect, round-robin turns, but that every thread does get at least some work time. And you can see that the consumers are not locked to specific producers. Again the idea is to make efficient use of limited resources. Producers can keep adding tasks without worrying about how long each task will take or who to assign it to. Consumers, in turn, can grab a task without worry about other consumers. If one consumer gets handed a simple task and finishes before other consumers, it can go back and get a new task right away.

Try running this example yourself and bump up some of those numbers. What happens with hundreds of URLs? What happens with hundreds of producers or consumers? At scale, this type of multitasking is almost required. You won’t find large programs out there than don’t use threads to manage at least some of their background work. Indeed, we saw above that Java’s own graphical package, Swing, needs a separate thread to keep the UI responsive and correct no matter how small your application might be.

Accessing class and instance Variables from Multiple Threads

In the SpreadSheet example, we guarded access to a set of instance variables with a synchronized method in order to avoid changing one of the variables while someone was reading the others. We wanted to keep them coordinated. But what about individual variable types? Do they need to be synchronized? Normally, the answer is no. Almost all operations on primitives and object reference types in Java happen atomically: that is, they are handled by the VM in one step, with no opportunity for two threads to collide. This prevents threads from looking at references while they are in the process of being accessed by other threads.

But watch out—we did say almost. If you read the Java VM specification carefully, you will see that the double and long primitive types are not guaranteed to be handled atomically. Both of these types represent 64-bit values. The problem has to do with how the Java VM’s stack handles them. It is possible that this specification will be beefed up in the future. But for now, to be strict, you should synchronize access to your double and long instance variables through accessor methods, or use the volatile keyword or an atomic wrapper class, which we’ll describe next.

Another issue, independent of the atomicity of the values, is the notion of different threads in the VM caching values for periods of time—that is, even though one thread may have changed the value, the Java VM may not be obliged to make that value appear until the VM reaches a certain state known as a “memory barrier.” You can start to address this by declaring the variable with the volatile keyword. This keyword indicates to the VM that the value may be changed by external threads and effectively synchronizes access to it automatically. We qualify that statement with “start to address” because multicore systems introduce yet more chances for inconsistent, buggy behavior. The closing paragraphs of “Concurrency Utilities” have some great reading suggestions if you have commercial development plans for your multithreaded code.

Finally, the java.util.concurrent.atomic package provides synchronized wrapper classes for all primitive types and references. These wrappers provide not only simple set() and get() operations on the values but also specialized “combo” operations, such as compareAndSet(), that work atomically and can be used to build higher-level synchronized application components. The classes in this package were designed specifically to map down to hardware-level functionality in many cases and can be very efficient.

Scheduling and Priority

Java makes few guarantees about how it schedules threads. Almost all of Java’s thread scheduling is left up to the Java implementation and, to some degree, the application. Although it might have made sense (and would certainly have made many developers happier) if Java’s developers had specified a scheduling algorithm, a single algorithm isn’t necessarily suitable for all the roles that Java can play. Instead, Java’s designers put the burden on you to write robust code that works no matter the scheduling algorithm, and let the implementation tune the algorithm for the best fit.4

The priority rules that we describe next are carefully worded in the Java language specification to be a general guideline for thread scheduling. You should be able to rely on this behavior overall (statistically), but it is not a good idea to write code that relies on very specific features of the scheduler to work properly. You should instead use the control and synchronization tools that we have described in this chapter to coordinate your threads.5

Every thread has a priority value. In general, any time a thread of a higher priority than the current thread becomes runnable (is started, stops sleeping, or is notified), it preempts the lower-priority thread and begins executing. By default, threads with the same priority are scheduled round-robin, which means once a thread starts to run, it continues until it does one of the following:

  • Sleeps, by calling Thread.sleep() or wait()

  • Waits for a lock, in order to run a synchronized method

  • Blocks on I/O, for example, in a read() or accept() call

  • Explicitly yields control, by calling yield()

  • Terminates by completing its target method6

This situation looks something like Figure 9-4.

lj5e 0904
Figure 9-4. Priority preemptive, round-robin scheduling

Thread State

At any given time, a thread is in one of five general states that encompass its lifecycle and activities. These states are defined in the Thread.State enumeration and queried via the getState() method of the Thread class:

NEW

The thread has been created but not yet started.

RUNNABLE

The normal active state of a running thread, including the time when a thread is blocked in an I/O operation, like a read or write or network connection.

BLOCKED

The thread is blocked, waiting to enter a synchronized method or code block. This includes the time when a thread has been awakened by a notify() and is attempting to reacquire its lock after a wait().

WAITING, TIMED_WAITING

The thread is waiting for another thread via a call to wait() or join(). In the case of TIMED_WAITING, the call has a timeout.

TERMINATED

The thread has completed due to a return, an exception, or being stopped.

We can show the state of all threads in Java (in the current thread group) with the following snippet of code:

    Thread [] threads = new Thread [ 64 ]; // max threads to show
    int num = Thread.enumerate( threads );
    for( int i = 0; i < num; i++ )
       System.out.println( threads[i] +":"+ threads[i].getState() );

You will probably not use this API in general programming, but it is interesting and useful for experimenting and learning about Java threads.

Time-Slicing

In addition to prioritization, all modern systems (with the exception of some embedded and “micro” Java environments) implement thread time-slicing. In a time-sliced system, thread processing is chopped up so that each thread runs for a short period of time before the context is switched to the next thread, as shown in Figure 9-5.

lj5e 0905
Figure 9-5. Priority preemptive, time-sliced scheduling

Higher-priority threads still preempt lower-priority threads in this scheme. The addition of time-slicing mixes up the processing among threads of the same priority; on a multiprocessor machine, threads may even be run simultaneously. This can introduce a difference in behavior for applications that don’t use threads and synchronization properly.

Strictly speaking, because Java doesn’t guarantee time-slicing, you shouldn’t write code that relies on this type of scheduling; any software you write should function under round-robin scheduling. If you’re wondering what your particular flavor of Java does, try the following experiment:

    public class Thready {
        public static void main( String args [] ) {
            new ShowThread("Foo").start();
            new ShowThread("Bar").start();
        }

        static class ShowThread extends Thread {
            String message;

            ShowThread( String message ) {
                this.message = message;
            }
            public void run() {
                while ( true )
                    System.out.println( message );
            }
        }
    }

The Thready class starts up two ShowThread objects. ShowThread is a thread that goes into a hard loop (very bad form) and prints its message. Because we don’t specify a priority for either thread, they both inherit the priority of their creator, so they have the same priority. When you run this example, you will see how your Java implementation does its scheduling. Under a round-robin scheme, only “Foo” should be printed; “Bar” never appears. In a time-slicing implementation, you should occasionally see the “Foo” and “Bar” messages alternate.

Priorities

As we said before, the priorities of threads exist as a general guideline for how the implementation should allocate time among competing threads. Unfortunately, with the complexity of how Java threads are mapped to native thread implementations, you cannot rely upon the exact meaning of priorities. Instead, you should only consider them a hint to the VM.

Let’s play with the priority of our threads:

    class Thready {
        public static void main( String args [] ) {
            Thread foo = new ShowThread("Foo");
            foo.setPriority( Thread.MIN_PRIORITY );
            Thread bar = new ShowThread("Bar");
            bar.setPriority( Thread.MAX_PRIORITY );

            foo.start();
            bar.start();
        }
    }

We would expect that with this change to our Thready class, the Bar thread would take over completely. If you run this code on an old Solaris implementation of Java 5.0, that’s what happens. The same is not true on Windows or with some older versions of Java. Similarly, if you change the priorities to values other than min and max, you may not see any difference at all. The subtleties relating to priority and performance relate to how Java threads and priorities are mapped to real threads in the OS. For this reason, thread priorities should be reserved for system and framework development.

Yielding

Whenever a thread sleeps, waits, or blocks on I/O, it gives up its time slot and another thread is scheduled. As long as you don’t write methods that use hard loops, all threads should get their due. However, a thread can also signal that it is willing to give up its time voluntarily at any point with the yield() call. We can change our previous example to include a yield() on each iteration:

    ...
    static class ShowThread extends Thread {
        ...
        public void run() {
            while ( true ) {
                System.out.println( message );
                yield();
            }
        }
    }

You should see “Foo” and “Bar” messages strictly alternating. If you have threads that perform very intensive calculations or otherwise eat a lot of CPU time, you might want to find an appropriate place for them to yield control occasionally. Alternatively, you might want to drop the priority of your compute-intensive thread so that more important processing can proceed around it.

Unfortunately, the Java language specification is very weak with respect to yield(). It is another one of those things that you should consider an optimization hint rather than a guarantee. In the worst case, the runtime system may simply ignore calls to yield().

Thread Performance

The way that applications use threads and the associated costs and benefits have greatly impacted the design of many Java APIs. We will discuss some of the issues in detail in other chapters. But it is worth briefly mentioning some aspects of thread performance and how the use of threads has dictated the form and functionality of several recent Java packages.

The Cost of Synchronization

The act of acquiring locks to synchronize threads takes time, even when there is no contention. In older implementations of Java, this time could be significant. With newer VMs, it is almost negligible. However, unnecessary low-level synchronization can still slow applications by blocking threads where legitimate concurrent access otherwise could be allowed. Because of this, two important APIs, the Java Collections API and the Swing GUI API, were specifically crafted to avoid unnecessary synchronization by placing it under the developer’s control.

The java.util Collections API replaces earlier, simple Java aggregate types—namely, Vector and Hashtable—with more fully featured and, notably, unsynchronized types (List and Map). The Collections API instead defers to application code to synchronize access to collections when necessary and provides special “fail fast” functionality to help detect concurrent access and throw an exception. It also provides synchronization “wrappers” that can provide safe access in the old style. Special concurrent-access-friendly implementations of the Map and Queue collections are included as part of the java.util.concurrent package. These implementations go even further in that they are written to allow a high degree of concurrent access without any user synchronization.

The Java Swing GUI has taken a different approach to providing speed and safety. Swing dictates that modification of its components (with notable exceptions) must all be done by a single thread: the main event queue. Swing solves performance problems as well as nasty issues of determinism in event ordering by forcing a single super-thread to control the GUI. The application may access the event queue thread indirectly by pushing commands onto a queue through a simple interface. We’ll see how to do just that in Chapter 10 and apply that knowledge to the common problem of reacting to information delivered externally over the network in Chapter 11.

Thread Resource Consumption

A fundamental pattern in Java is to start many threads to handle asynchronous external resources, such as socket connections. For maximum efficiency, a web server might be tempted to create a thread for each client connection it is servicing. With each client having its own thread, I/O operations may block and restart as needed. But as efficient as this may be in terms of throughput, it is a very inefficient use of server resources. Threads consume memory; each thread has its own “stack” for local variables, and switching between running threads (context switching) adds overhead to the CPU. While threads are relatively lightweight (in theory, it is possible to have hundreds or thousands running on a large server), at a certain point, the resources consumed by the threads themselves start defeating the purpose of starting more threads. Often, this point is reached with only a few dozen threads. Creating a thread per client is not always a scalable option.

An alternative approach is to create “thread pools” where a fixed number of threads pull tasks from a queue and return for more when they are finished. This recycling of threads makes for solid scalability, but it has historically been difficult to implement efficiently for servers in Java because stream I/O (for things like sockets) has not fully supported nonblocking operations. The NIO package has asynchronous I/O channels: nonblocking reads and writes plus the ability to “select” or test the readiness of streams for moving data. Channels can also be asynchronously closed, allowing threads to work with them gracefully. With the NIO package, it is possible to create servers with much more sophisticated, scalable thread patterns.

Thread pools and job “executor” services are codified as utilities as part of the java.util.concurrent package, meaning you don’t have to write these yourself. We’ll summarize them next when we discuss the concurrency utilities in Java.

Concurrency Utilities

So far in this chapter, we’ve demonstrated how to create and synchronize threads at a low level, using Java language primitives. The java.util.concurrent package and subpackages introduced with Java 5.0 build on this functionality, adding important threading utilities and codifying some common design patterns by supplying standard implementations. Roughly in order of generality, these areas include:

Thread-aware Collections implementations

The java.util.concurrent package augments the Java Collections API in Chapter 7 with several implementations for specific threading models. These include timed wait and blocking implementations of the Queue interface, as well as nonblocking, concurrent-access optimized implementations of the Queue and Map interfaces. The package also adds “copy on write” List and Set implementations for extremely efficient “almost always read” cases. These may sound complex, but actually cover some fairly simple cases very well.

Executors

Executors run tasks, including Runnables, and abstract the concept of thread creation and pooling from the user. Executors are intended to be a high-level replacement for the idiom of creating new threads to service a series of jobs. Along with Executors, the Callable and Future interfaces are introduced, which expand upon Runnable to allow management, value return, and exception handling.

Low-level synchronization constructs

The java.util.concurrent.locks package holds a set of classes, including Lock and Condition, that parallels the Java language-level synchronization primitives and promotes them to the level of a concrete API. The locks package also adds the concept of nonexclusive reader/writer locks, allowing for greater concurrency in synchronized data access.

High-level synchronization constructs

This includes the classes CyclicBarrier, CountDownLatch, Semaphore, and Exchanger. These classes implement common synchronization patterns drawn from other languages and systems and can serve as the basis for new high-level tools.

Atomic operations (sounds very James Bond, doesn’t it?)

The java.util.concurrent.atomic package provides wrappers and utilities for atomic, “all-or-nothing” operations on primitive types and references. This includes simple combination atomic operations like testing a value before setting it and getting and incrementing a number in one operation.

With the possible exception of optimizations done by the Java VM for the atomic operations package, all of these utilities are implemented in pure Java, on top of the standard Java language synchronization constructs. This means that they are in a sense only convenience utilities and don’t truly add new capabilities to the language. Their main role is to offer standard patterns and idioms in Java threading and make them safer and more efficient to use. A good example of this is the Executor utility, which allows a user to manage a set of tasks in a predefined threading model without having to delve into creating threads at all. Higher-level APIs like this both simplify coding and allow for greater optimization of the common cases.

While we won’t be looking at any of these packages in this chapter, we want you to know where you might dig next if concurrency is interesting to you or seems useful in the type of problems you need to solve at work. As we (foot)noted in “Synchronizing a queue of URLs”, “Java Concurrency In Practice” by Brian Goetz, is required reading for real-world projects. We also want to give a shout-out to Doug Lea, the author of Concurrent Programming in Java (Addison-Wesley), who led the group that added these packages to Java and is largely responsible for creating them.

We have mentioned the Java Swing framework in passing several times in this book—even in this chapter with respect to thread performance. Next up it is finally time to look at that framework in more detail.

1 interrupt() has not worked consistently in all Java implementations historically.

2 Don’t confuse the term serialize in this context with Java object serialization, which is a mechanism for making objects persistent. The underlying meaning (to place one thing after another) does apply to both, however. In the case of object serialization, the object’s data is laid out, byte for byte, in a certain order.

3 Even with fault tolerance, modern, multicore systems can wreak havoc on systems without perfect knowledge. And perfection is difficult! If you expect to work with threads in the real world, “Java Concurrency In Practice” by Brian Goetz, is required reading.

4 A notable alternative to this is the real-time Java specification that defines specialized thread behavior for certain types of applications. It was developed under the Java community process and can be found at https://rtsj.dev.java.net/.

5 Java Threads by Scott Oaks and Henry Wong (O’Reilly) includes a detailed discussion of synchronization, scheduling, and other thread-related issues.

6 Technically, a thread can also terminate with the deprecated stop() call but as we noted at the start of the chapter, this is bad for myriad reasons.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.16.90