ThreadLocal


You may have a need to store separate information along with each thread that executes. Java provides a class named ThreadLocal that manages creating and accessing a separate instance, per thread, of any type.

When you interact with a database through JDBC,[14] you obtain a Connection object that manages communication between your application and the database. It is not safe for more than one thread to work with a single connection object, however. One classic use of ThreadLocal is to allow each thread to contain a separate Connection object.

[14] The Java DataBase Connectivity API. See Additional Lesson III for further information.

However, you've not learned about JDBC yet. For your example, I'll define an alternate use for ThreadLocal.

In this example, you'll take the existing Server class and add logging capabilities to it. You want each thread to track when the search started and when it completed. You also want the start and stop message pairs for a search to appear together in the log. You could store a message for each event (start and stop) in a common thread-safe list stored in Server. But if several threads were to add messages to this common list, the messages would interleave. You'd probably see all the start log events first, then all the stop log events.

To solve this problem, you can have each thread store its own log messages in a ThreadLocal variable. When the thread completes, you can obtain a lock and add all the messages at once to the complete log.

First, modify the Server class to create a new thread for each Search request. This should boost the performance in most environments, but you do want to be careful how many threads you let execute simultaneously. (You might experiment with the number of searches the test kicks off to see what the limits are for your environment.) You may want to consider a thread pool as mentioned in the section Additional Notes on wait and notify.

 private void execute(final Search search) {    new Thread(new Runnable() {       public void run() {          search.execute();          listener.executed(search);       }    }).start(); } 

Ensure that your tests still run successfully. Next, let's refactor ServerTestsignificantly. You want to add a new test that verifies the logged messages. The current code is a bit of a mess, and it has a fairly long and involved test. For the second test, you will want to reuse most of the nonassertion code from testSearch. The refactored code also simplifies a bit of the performance testing.

Here is the refactored test that includes a new test, testLogs.

 package sis.search; import junit.framework.*; import java.util.*; import sis.util.*; public class ServerTest extends TestCase {    // ...    public void testSearch() throws Exception {       long start = System.currentTimeMillis();       executeSearches();       long elapsed = System.currentTimeMillis() - start;       assertTrue(elapsed < 20);       waitForResults();    }    public void testLogs() throws Exception {       executeSearches();       waitForResults();       verifyLogs();    }    private void executeSearches() throws Exception {       for (String url: URLS)          server.add(new Search(url, "xxx"));    }    private void waitForResults() {       long start = System.currentTimeMillis();       while (numberOfResults < URLS.length) {          try {Thread.sleep(1); }          catch (InterruptedException e) {}          if (System.currentTimeMillis() - start > TIMEOUT)             fail("timeout");       }    }    private void verifyLogs() {       List<String> list = server.getLog();       assertEquals(URLS.length * 2, list.size());       for (int i = 0; i < URLS.length; i += 2)          verifySameSearch(list.get(i), list.get(i + 1));    }    private void verifySameSearch(          String startSearchMsg, String endSearchMsg) {       String startSearch = substring(startSearchMsg, Server.START_MSG);       String endSearch = substring(endSearchMsg, Server.END_MSG);       assertEquals(startSearch, endSearch);    }    private String substring(String string, String upTo) {       int endIndex = string.indexOf(upTo);       assertTrue("didn't find " + upTo + " in " + string,          endIndex != -1);       return string.substring(0, endIndex);    } } 

The method testLogs executes the searches, waits for them to complete, and verifies the logs. To verify the logs, the test requests the complete log from the Server object, then loops through the log, extracting a pair of lines at a time. It verifies that the search string (the first part of the log message) is the same in each pair of lines.[15]

[15] See Lesson 8 for an in-depth discussion of Java logging and a discussion of whether or not it is necessary to test logging code.

The modifications to the Server class:

 package sis.search; import java.util.concurrent.*; import java.util.*; public class Server extends Thread {    private BlockingQueue<Search> queue =       new LinkedBlockingQueue<Search>();    private ResultsListener listener;    static final String START_MSG = "started";    static final String END_MSG = "finished";    private static ThreadLocal<List<String>> threadLog =       new ThreadLocal<List<String>>() {          protected List<String> initialValue() {             return new ArrayList<String>();          }       };    private List<String> completeLog =       Collections.synchronizedList(new ArrayList<String>());    // ...    public List<String> getLog() {       return completeLog;    }    private void execute(final Search search) {       new Thread(new Runnable() {          public void run() {             log(START_MSG, search);             search.execute();             log(END_MSG, search);             listener.executed(search);             completeLog.addAll(threadLog.get());          }       }).start();    }    private void log(String message, Search search) {       threadLog.get().add(          search + " " + message + " at " + new Date());    }    // ... } 

You declare a ThreadLocal object by binding it to the type of object you want stored in each thread. Here, tHReadLog is bound to a List of String objects. The threadLog ThreadLocal instance will internally manage a List of String objects for each separate thread.

You can't simply assign an initial value to tHReadLog, since each of the List objects it manages need to be initialized. Instead, ThreadLocal provides an initialValue method that you can override. The first time each thread gets its ThreadLocal instance via threadLog, code in the ThreadLocal class calls the initialValue method. Overriding this method provides you with the opportunity to consistently initialize the list.

ThreadLocal defines three additional methods: get, set, and remove. The get and set methods allow you to access the current thread's ThreadLocal instance. The remove method allows you to remove the current thread's ThreadLocal instance, perhaps to save on memory space.

In the search thread's run method, you call the log method before and after executing the search. In the log method, you access the current thread's copy of the log list by calling the get method on threadLog. You then add a pertinent log string to the list.

Once the search thread completes, you want to add the thread's log to the complete log. Since you've instantiated completeLog as a synchronized collection, you can send it the addAll method to ensure that all lines in the thread log are added as a whole. Without synchronization, another thread could add its log lines, resulting in an interleaved complete log.



Agile Java. Crafting Code with Test-Driven Development
Agile Javaв„ў: Crafting Code with Test-Driven Development
ISBN: 0131482394
EAN: 2147483647
Year: 2003
Pages: 391
Authors: Jeff Langr

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net