站内搜索: 请输入搜索关键词
当前页面: 图书首页 > Java Threads, Third Edition

8.2 Synchronization and Collection Classes - Java Threads, Third Edition

Previous Section  < Day Day Up >  Next Section

8.2 Synchronization and Collection Classes

When writing a multithreaded program, the most important question when using a collection class is how to manage its synchronization. Synchronization can be managed by the collection class itself or managed explicitly in your program code. In the examples in this section, we'll explore both of these options.

8.2.1 Simple Synchronization

Let's take the simple case first. In the simple case, you're going to use the collection class to store shared data. Other threads retrieve data from the collection, but there won't be much (if any) manipulation of the data.

In this case, the easiest object to use is a threadsafe collection (e.g., a Vector or Hashtable). That's what we've done all along in our CharacterEventHandler class:

package javathreads.examples.ch08.example1;



import java.util.*;



public class CharacterEventHandler {

    private Vector listeners = new Vector( );



    public void addCharacterListener(CharacterListener cl) {

        listeners.add(cl);

    }



    public void removeCharacterListener(CharacterListener cl) {

        listeners.remove(cl);

    }



    public void fireNewCharacter(CharacterSource source, int c) {

        CharacterEvent ce = new CharacterEvent(source, c);

        CharacterListener[] cl = (CharacterListener[] )

                                  listeners.toArray(new CharacterListener[0]);

        for (int i = 0; i < cl.length; i++)

            cl[i].newCharacter(ce);

    }

}

In this case, using a vector is sufficient for our purposes. If multiple threads call methods of this class at the same time, there is no conflict. Because the listeners collection is threadsafe, we can call its add(), remove(), and toArray() methods at the same time without corrupting the internal state of the Vector object. Strictly speaking, there is a race condition here in our use of the toArray() method; we'll talk about that a little more in the next section. But the point is that none of the methods on the vector see data in an inconsistent state because the Vector class itself is threadsafe.

A second option would be to use a thread-unsafe class (e.g., the ArrayList class) and manage the synchronization explicitly:

package javathreads.examples.ch08.example2;

...

public class CharacterEventHandler {

    private ArrayList listeners = new ArrayList( );

    public synchronized void addCharacterListener(CharacterListener cl) {

        ...

    }

    public synchronized void removeCharacterListener(CharacterListener cl) {

        ...

    }

    public synchronized void fireNewCharacter(CharacterSource source, int c) {

        ...

    }

}

Or we could have synchronized the class like this:

package javathreads.examples.ch08.example3;

...

public class CharacterEventHandler {

    private ArrayList listeners = new ArrayList( );



    public void addCharacterListener(CharacterListener cl) {

        synchronized(listeners) {

            listeners.add(cl);

        }

    }



    public void removeCharacterListener(CharacterListener cl) {

        synchronized(listeners) {

            listeners.add(cl);

        }

    }

    public void fireNewCharacter(CharacterSource source, int c) {

        CharacterEvent ce = new CharacterEvent(source, c);

        CharacterListener[] cl;

        synchronized(listeners) {

            cl = (CharacterListener[])

                                  listeners.toArray(new CharacterListener[0]);

        }

        for (int i = 0; i < cl.length; i++)

            cl[i].newCharacter(ce);

    }

}

In this example, it doesn't matter whether we synchronize on the collection object or the event handler object (this); either one ensures that two threads are not simultaneously calling methods of the ArrayList class.

Our third option is to use a synchronized version of the thread-unsafe collection class. Most thread-unsafe collection classes have a synchronized counterpart that is threadsafe. The threadsafe collections are constructed by calling one of these static methods of the Collections class:

Set s = Collections.synchronizedSet(new HashSet(...));

Set s = Collections.synchronizedSet(new LinkedHashSet(...));

SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

Set s = Collections.synchronizedSet(EnumSet.noneOf(obj.class));

Map m = Collections.synchronizedMap(new HashMap(...));

Map m = Collections.synchronizedMap(new LinkedHashMap(...));

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

Map m = Collections.synchronizedMap(new WeakHashMap(...));

Map m = Collections.synchronizedMap(new IdentityHashMap(...));

Map m = Collections.synchronizedMap(new EnumMap(...));

List list = Collections.synchronizedList(new ArrayList(...));

List list = Collections.synchronizedList(new LinkedList(...));

Any of these options protect access to the data held in the collection. This is accomplished by wrapping the collection in an object that synchronizes every method of the collection interface: it is not designed as an optimally synchronized class. Also note that the queue collection is not supported: the Collections class supplies only wrapper classes that support the Set, Map, and List interfaces. This is not a problem in most cases since the majority of the queue implementations are synchronized (and synchronized optimally).

8.2.2 Complex Synchronization

A more complex case arises when you need to perform multiple operations atomically on the data held in the collection. In the previous section, we were able to use simple synchronization because the methods that needed to access the data in the collection performed only a single operation. The addCharacterListener() method has only a single statement that uses the listeners vector, so it doesn't matter if the data changes after the addCharacterListener( ) method calls the listeners.add() method. As a result, we could rely on the container to provide the synchronization.

We alluded to a race condition in the fireNewCharacter() method. After we call the listeners.toArray( ) method, we cycle through the listeners to call each of them. It's entirely possible that another thread will call the removeCharacterListener() method while we're looping through the array. That won't corrupt the array or the listeners vector, but in some algorithms, it could be a problem: we'd be operating on data that has been removed from the vector. In our program, that's okay: we have a benign race condition. In other programs, that may not necessarily be the case.

Suppose we want to keep track of all the characters that players typed correctly (or incorrectly). We could do that with the following:

package javathreads.examples.ch08.example4;



import java.util.*;

import javax.swing.*;

import javax.swing.table.*;



public class CharCounter {

    public HashMap correctChars = new HashMap( );

    public HashMap incorrectChars = new HashMap( );

    private AbstractTableModel atm;



    public void correctChar(int c) {

        synchronized(correctChars) {

            Integer key = new Integer(c);

            Integer num = (Integer) correctChars.get(key);

            if (num == null)

                correctChars.put(key, new Integer(1));

            else correctChars.put(key, new Integer(num.intValue( ) +1));

            if (atm != null)

                atm.fireTableDataChanged( );

        }

    }



    public int getCorrectNum(int c) {

        synchronized(correctChars) {

            Integer key = new Integer(c);

            Integer num = (Integer) correctChars.get(key);

            if (num == null)

                return 0;

            return num.intValue( );

        }

    }



    public void incorrectChar(int c) {

        synchronized(incorrectChars) {

            Integer key = new Integer(c);

            Integer num = (Integer) incorrectChars.get(key);

            if (num == null)

                incorrectChars.put(key, new Integer(-1));

            else incorrectChars.put(key, new Integer(num.intValue( ) -1));

            if (atm != null)

                atm.fireTableDataChanged( );

        }

    }



    public int getIncorrectNum(int c) {

        synchronized(incorrectChars) {

            Integer key = new Integer(c);

            Integer num = (Integer) incorrectChars.get(key);

            if (num == null)

                return 0;

            return num.intValue( );

        }

    }



    public void addModel(AbstractTableModel atm) {

        this.atm = atm;

    }

}

Here we use thread-unsafe collections to hold the data and explicitly synchronize access around the code that uses the collections. It would be insufficient to use Hashtable collections in this code without also synchronizing as we did earlier. Although retrieving a value from a hashtable is threadsafe, and replacing an element in a hashtable is also threadsafe, the overall operation is not threadsafe: both collection operations must be atomic for the algorithm to succeed. Otherwise, two threads could simultaneously retrieve the stored value, increment it, and store it; the net result would be a score that is one less than it should be.

The moral of the story is that using a threadsafe collection does not guarantee the correctness of your program. Because of the explicit synchronization required in this example, we were able to use a thread-unsafe collection (although, as we'll see in Chapter 14, if you use a threadsafe collection, it's unlikely you'll see much difference.)

8.2.3 Iterators and Enumerations

Many situations call for using each element of a collection. Such is the case in our example. We called the toArray() method, which returns an array containing every element in the vector. The Vector and Hashtable classes also have methods that return a java.util.Enumeration object that contains every element in the collection. More generally, all collection classes implement one or more methods that return a java.util.Iterator object. The iterator also contains every element in the collection.

Each of these techniques presents special synchronization concerns. We've already seen that looping through the array returned by the toArray() method can lead to a situation where we're accessing an element in the array that no longer appears in the collection. That may or may not be a problem for your program; if it is a problem, the solution is to synchronize access around the loop that uses the array.

Enumeration objects are difficult to use without explicit synchronization. The enumeration keeps state information about the collection; if the collection is modified while the enumeration is active, the enumeration may become confused. The enumeration fails in some random way, possibly through an unexpected runtime exception (e.g., a NullPointerException).

To use an enumeration of a collection that may also be used by multiple threads, you should synchronize on the collection object itself:

package javathreads.examples.ch08.example5;

...

    public void fireNewCharacter(CharacterSource source, int c) {

        CharacterEvent ce = new CharacterEvent(source, c);

        Enumeration e;

               synchronized(listeners) {

               e = listeners.elements( );

               while (e.hasMoreElements( )) {

                       ((CharacterListener) e.nextElement( )).newCharacter(ce);

                  }

         }

    }

}

You could synchronize the method instead, as long as your collection is not used in any unsynchronized method. The point is that the enumeration and all uses of the collection must be locked by the same synchronization object.

Iterators behave somewhat differently. If the underlying collection of an iterator is modified while the iterator is active, the next access to the iterator throws a ConcurrentModificationException, which is also a runtime exception. Unlike enumerations, if the iterator fails, the underlying collection can still be used. The way in which iterators fail immediately after a modify operation is called "fail-fast."

The safest way to use an iterator is to make sure its use is synchronized by its underlying collection (just as we did with the enumeration)梠r to make sure that it and the collection are protected by the same synchronization lock.

You can't rely upon the fail-fast nature of iterators. Iterators make a best effort at determining when the underlying collection has changed, but in the absence of synchronization, it's impossible to predict when the failure occurs. Once a failure has occurred, the iterator is not useful for further processing. Therefore, you're left with a situation where some elements of the collection have been processed and others have not.

Two classesCopyOnWriteArrayList and CopyOnWriteArraySet梡rovide special iteration semantics. These classes are designed to copy the underlying collection when necessary so that iterators operate on a snapshot of the data from the time the iterator was created. Modifying the collection while the iterator is active creates a copy of the collection for the iterator.

This is an expensive operation, both in terms of time and memory usage. However, it ensures that iterators can be used from unsynchronized code because the iterators end up operating on old copies of the data. So, the iterators never throw a concurrent modification exception.

These classes are designed for cases where modifications to the collection are rare and the iterator of the collection is used frequently by multiple threads. This allows the iterators to be unsynchronized and still be threadsafe; as long as the updates are rare enough, this yields better overall performance. Note, however, that race conditions are still possible with this technique; it's essentially the same type of operation as we saw earlier with the toArray() method. The difference is when the copying occurs: when you call the toArray() method, a copy of the collection is made at that time. With the copy-on-write classes, the copy is made whenever the collection is modified.

8.2.4 Thread-Aware Classes

Many collection classes are what we would term "thread-aware." They have many internal and subtle features that were designed specifically for threads:

  • Some collections have an implementation that minimizes the need for synchronization by segmenting the collection. It is possible for threads to modify the collection simultaneously, without any synchronization, when they are operating on different segments.

  • Some provide special services梥uch as iterator handling梩hat are specifically designed for multithreaded environments. The main reason for copy-on-write iterators is to balance the performance issues of many simultaneous threads iterating through the collection against a few updates to the collection.

  • Interfaces have been enhanced to handle issues related to threads better. For example, the concurrent hashmap has the ability to add a key only if the key is not in the map; this simple enhancement removes the need for explicit synchronization for parallel writes of new elements.

    Previous Section  < Day Day Up >  Next Section