Threading and Synchronization

 < Free Open Study > 



Web applications are not single-user systems. Any servlet that we write may need to cope with concurrent requests from remote clients. We'd like those requests to be handled with no side effects resulting from multiple users trying to do the same thing at the same time. In this section we'll look at some of the issues involved in this, particularly how to use:

  • Threading to improve scalability and throughput

  • Synchronization to ensure data integrity

We'll investigate the effects of container threading on application logic. While multi-threading can have a very positive impact on the performance and scalability of an application, it contains traps for the unwary. Even if we play it safe with single-threaded servlets, we might still need to rethink the way we use instance and class variables to propagate state information between method invocations.

We won't be creating and managing any threads of our own, rather we'll be letting the container use its own threading model with just am instruction from us as to whether our servlets should be single-threaded or multi-threaded. All you really need to understand about threads is that they allow a single piece of code to be executed many times, concurrently, within a single process. A method on a single instance may be executed in parallel on separate threads, with each thread maintaining its own progress through the method.

A Banking Application

We'll implement a very simple web-based banking service, first as a single-threaded servlet and then as a multi-threaded servlet. It will allow money transfers between bank accounts, and the results of each invocation will show the latest balance in each account and a list of transactions.

This will be a multi-user application, with many online users wanting to perform transactions at the same time. As a banking application, those users will want the integrity of the data to be maintained. I certainly would.

Using a Single-Threaded Servlets

We'll start with a single-threaded version of the servlet as the simplest case. We'll ensure that our servlet really is single-threaded by implementing the javax.servlet.SingleThreadModel interface.

start sidebar

Implementing the javax.servlet.SingleThreadModel interface guarantees that no two threads will execute concurrently in the servlet's service() method.

end sidebar

    package synchronization;    import java.io.*;    import java.util.*;    import javax.servlet.*;    import javax.servlet.http.*;    public class SingleThreadBankServlet extends HttpServlet        implements SingleThreadModel { 

We have two instance variables. One to hold the balances of the accounts at the bank and one to hold the list of user transactions:

       private Hashtable accounts;       private Vector userTransactions; 

When the servlet is initialized we'll reset the state of the member variables by calling the resetValues() method:

        public void init() throws ServletException {          resetValues();        } 

In the doGet() method we'll find out what the user wants to do by looking at the request parameters. We're also calling an in() method on a separate class called InOut (that will be explained shortly). As we've implemented the SingleThreadModel interface, only one request thread can run this service method at any one time:

        public void doGet (HttpServletRequest req, HttpServletResponse res)            throws ServletException, IOException {          InOut.in();          String reset = req.getParameter("reset");          String fromAccount = req.getParameter("from");          String toAccount = req.getParameter("to");          String amount = req.getParameter("amount");          PrintWriter writer = res.getWriter();          writer.println("<html><head></head>");          writer.println("<body style=\"font-family:verdana;font-size:8pt\">"); 

If the user supplied a reset parameter, we reset the state of the member variables (this is for convenience during testing):

          if (reset != null) {            resetValues();          } 

If the user specifies fromAccount, toAccount, and amount parameters, we perform a money transfer by calling the transfer() method:

          if ((fromAccount != null) && (toAccount != null) && (amount != null)) {            transfer(fromAccount, toAccount, amount);          } 

In every case we show the latest account balances and the list of user transactions via the showData() method:

           showData(writer); 

Finally we make a call to the out() method of the InOut class:

         writer.println("</body></html>");         writer.close();         InOut.out();       } 

The resetValues() method simply initializes the member variables to a known state:

       public void resetValues() throws ServletException {         userTransactions = new Vector();         accounts = new Hashtable();         accounts.put("000001", "1000");         accounts.put("000002", "1000");       } 

The showData() method produces output for the latest account balances and the history of user transactions (in reverse order with the latest transaction in bold):

       private void showData(PrintWriter writer) {         writer.println("<b>Account Balances</b><br><br>");         for (Enumeration keys = accounts.keys(); keys.hasMoreElements();) {           String thisKey = (String) keys.nextElement();           String thisBalance = (String) accounts.get(thisKey);           writer.println("Account " + thisKey + " has balance " +                          thisBalance + "<br>");         }         writer.println("<br><b>User Transactions</b><br><br>");         int i = userTransactions.size() - 1;         if (i >= 0) {           writer.println("<b>" + userTransactions.elementAt(i) + "</b><br>");         }         while (--i >= 0) {           writer.println("" + userTransactions.elementAt(i) + "<br>");         }       } 

The transfer() method simply reads the values from the toAccount and fromAccount request parameters, calculates new account balances based on the transfer amount, and then writes the new balances back into the accounts hashtable. We've done that in a rather convoluted way because we want to increase the chance of a conflict, but just to make sure we also include a five second delay:

       private void transfer(String fromAccount, String toAccount, String amount) {         int fromAccountBalance =                             new Integer((String)accounts.get(fromAccount)).intValue();         int newFromAccountBalance =                             fromAccountBalance - (new Integer(amount).intValue());         int toAccountBalance =                             new Integer((String)accounts.get(toAccount)).intValue();         int newToAccountBalance = toAccountBalance + (new Integer(amount).intValue());         try {           Thread.sleep(5000);         } catch (java.lang.InterruptedException ex) {}         accounts.put(fromAccount, "" + newFromAccountBalance);         accounts.put(toAccount, "" + newToAccountBalance);         String transaction = "Transfer of " + amount + " from account " +                              fromAccount + " to account " + toAccount;         userTransactions.add(transaction);       }     } 

In this single-threaded servlet we're not expecting a conflict at all, but we'll be taking exactly the same code forward to a multi-threaded scenario to show the difference in behavior.

Finally, you're probably wondering about the InOut class that we called at the beginning and end of the doGet() method. It provides something for our debugger (from Chapter 10) to latch onto as we conduct our tests:

     package synchronization;     public class InOut {       public static void in() {}       public static void out() {}     } 

Deploying the Application

We'll deploy this application in a context called Synchronization:

      Synchronization/                      WEB-INF/                              classes/                                      SingleThreadBankServlet.class                                      InOut.class 

If you want to reproduce the debug trace we show in this section you'll need to run Tomcat in debug mode (as described in Chapter 10).

Using Single-Threaded Servlets

Launch two browser windows, and navigate in each to http://localhost:8080/Synchronization/servlet/synchronization.SingleThreadBankServlet?reset=true. Each browser window will show this:

click to expand

Now we can make use of the JPDA Debugger we created in Chapter 10 to record what really happens in the code. Start the debugger with this command:

     java debugger.JPDADebugger -attach localhost:8124 -include synchronization.InOut >     singlethread.txt 

We're including our InOut class as the -include parameter so the debugger will record all entries to the servlet's doGet() method (InOut.in()) and all exits (InOut.out()).

Now transfer 500 from account 000001 to account 000002 in both browsers by navigating to http://localhost:8080/Synchronization/servlet/synchronization.SingleThreadBankServlet?from=000001&to=000002&amount=500. In the first window we see this:

click to expand

In the second window we see this:

click to expand

That result is just what we would expect. To clarify the sequence of events we can look at the debug trace using the debug trace viewer from Chapter 10:

click to expand

Notice that execution went into the servlet doGet() method, and back out again, on thread HttpProcessor[8080][4] before going in and out of the same doGet() method of the same servlet on thread HttpProcessor[8080][3]. The two threads are executed in strict order. The threads are only allowed into the service method of the servlet one-at-a-time.

Servlet Pooling

Although Tomcat 4 queues requests to servlets that implement SingleThreadModel, some other servlet containers (such as WebLogic) improve throughput by starting up additional servlet instances in response to multiple client requests.

start sidebar

Implementing SingleThreadModel flags up to certain servlet containers that a servlet may be pooled.

end sidebar

To reduce the cost of starting up new instances of servlets each time, and to limit the total number of instances, servers use a servlet pool. At startup, a number of servlets of each type are created in the pool. As each client request comes in, one of the unused servlet instances from the pool is allocated to handle the request. On completion that instance is returned to the pool for use. This way, even servlets that implement SingleThreadModel can service concurrent requests (up to the limit of the pool size).

This has implications for instance variables. Developers often use an instance variable in servlets to record information (such as hit counts) across all users of the servlet, on the assumption that there will only ever be one servlet instance handling multiple requests on separate threads. The same situation exists in our SingleThreadBankServlet example, where we assume that the accounts hashtable holds a common view of account balances for all users. In a pooled scenario the account balances that we see would depend on which servlet instance handled our request.

start sidebar

If servlets that implement SingleThreadModel are pooled we cannot rely on a single set of instance variables to be common to all users of the servlet.

end sidebar

If you're using Tomcat 4 you might decide to relax because it does not support servlet pooling, but your servlet could well behave quite differently when deployed in a different container.

Multi-Threaded Servlets

The problem with using SingleThreadModel is that all requests to the servlet are fed through a single servlet instance, and so must be queued if that instance is busy servicing another request. This is not desirable in an web application that must simultaneously support many users.

start sidebar

Throughput can be significantly improved by allowing our servlets to process multiple concurrent requests on separate threads.

end sidebar

To make our sample servlet multi-threaded we simply remove the implements SingleThreadModel statement from our code and rename our servlet to BankServlet:

     public class BankServlet extends HttpServlet 

That's the only change we'll need to make before running through our tests again.

Using Multi-Threaded Servlets

Let's look at what happens when we step through the same sequence of events as before, this time with the multi-threaded servlet. The end result in the first window looks like this:

click to expand

In the second window the result looks like this:

click to expand

Clearly there is a problem. We've transferred a total of 1000 from account 000001 to account 000002 yet the final balances do not reflect this. To clarify why this problem has occurred we should look at the debug trace:

click to expand

We can see that the two threads are intertwined as they progress through the servlet doGet() method. This means that we've read the old account balances in the second thread before writing the new account balances in the second thread. This is a classic race condition.

start sidebar

In multi-threaded servlets the use of instance variables may be inconsistent and unpredictable.

end sidebar

We introduced an artificial delay in the code to ensure that this would happen, but this is the type of problem that could easily occur in a multi-user environment.

Explicit Synchronization

The use of instance variables can cause problems whether we're multi-threaded or single-threaded-with-pooling, but for two different reasons:

  • For single-threaded servlets the problem is that pooled servlets will maintain separate sets of instance variables, unless we take the performance hit of not using a servlet pool

  • For multi-threaded servlets the problem is that race conditions may result in unpredictable values in instance variables

It's best to avoid the use of instance variables in servlets whenever possible, but clearly they were used in the first place for a good reason - to propagate information between servlet invocations.

If you decide that you really can't do without instance variables, one possible solution to the second problem is to use a multi-threaded servlet with the critical sections of code explicitly synchronized to make them thread-safe. In our example the critical code is within the transfer() method so we can synchronize it by changing the method definition to this:

     private synchronized void transfer(String fromAccount, String toAccount,                                        String amount) 

You might be tempted to synchronize the servlet's service methods, doGet() and doPost(). However, the Servlet 2.3 specification advises against this approach because it offers the disadvantage of the SingleThreadModel (all servlet requests are serialized) without providing the benefit that some servers may employ a pooling mechanism to improve throughput.

If we run through our tests with the synchronized method, the end result (in the browser windows) is the same as for the single-threaded servlet. However, the debug trace shows that the servlet requests themselves have not been serialized. We get a debug trace similar to that of the non-synchronized multi-threaded servlet:

click to expand

This approach would work if were meticulous in explicitly synchronizing all methods that may compete for instance variables. In addition, the throughput will be higher than a SingleThreadModel (fully synchronized) servlet if we keep the synchronized parts of our code as small as possible.

Servlet using Session and Context Attributes

Say that we want to be able to record a list of transactions performed by a particular end user during a particular session. We don't want to do it via an instance variable because instance variables are per servlet, not per client, so for a multi-threaded servlet there would only ever be one list of transactions regardless of the number of end users. For a single-threaded servlet (with pooling) we have no way of knowing how many instances the server will create to handle the load.

Let's say we would also like to be able to record the latest account balances. Again, we don't want to do this via an instance variable that is specific to an individual servlet instance, or in a static class variable whose exact scope will be determined by the class loading mechanism.

There is a way to handle these two situations:

  • Session attributes - these support the propagation of information relating to a particular end user's sequence of events, and isolated from are information relating to other end users

  • Context attributes - these support the propagation of information between a group of components deployed within the same web context, even in a distributed scenario

We'll concentrate on the implications of concurrent access to context and session attributes for our banking servlet - you can learn more about sessions in general in Chapter 5. We'll use a version of BankServlet called SyncSessionBankServlet that is adapted to store the accounts balances as a context attribute and the list of user transactions as a session attribute:

     package synchronization;     import java.io.*;     import javax.servlet.*;     import javax.servlet.http.*;     import java.util.*;     public class SyncSessionBankServlet extends HttpServlet {       private ServletContext servletContext = null; 

The transfer() method has changed to take in the current HTTP request as a parameter. This is so that we can get hold of the current user session:

       private void transfer(String fromAccount, String toAccount, String amount,           HttpServletRequest req) { 

We get the accounts hashtable from the application's servlet context and read the old account balances as before. The accounts hashtable gets into the context via a modified version of the resetValues():

         Hashtable accounts = (Hashtable) servletContext.getAttribute("accounts");         if (accounts == null) {           return;         }         int fromAccountBalance =                             new Integer((String)accounts.get(fromAccount)).intValue();         int newFromAccountBalance =                             fromAccountBalance - (new Integer(amount).intValue());         int toAccountBalance =                             new Integer((String)accounts.get(toAccount)).intValue();         int newToAccountBalance = toAccountBalance + (new Integer(amount).intValue()); 

We retain the delay of five seconds so that we can test the race condition later:

         try {           Thread.sleep(5000);         } catch (java.lang.InterruptedException ex) {} 

We put the new account balances back into the context:

         accounts.put(fromAccount, "" + newFromAccountBalance);         accounts.put(toAccount, "" + newToAccountBalance);         servletContext.setAttribute("accounts", accounts); 

We get hold of the user session and extract the list of transactions for this user, or start a new list if no transactions have been performed:

         HttpSession session = req.getSession(true);         Vector userTransactions = (Vector) session.getAttribute("transactions");         if (userTransactions == null) {           userTransactions = new Vector();         } 

Finally, we add the current transaction and put the list back as a session attribute:

         String transaction = "Transfer of " + amount + " from account " +                              fromAccount + " to account " + toAccount;         userTransactions.add(transaction);         session.setAttribute("transactions", userTransactions);       } 

The logic is exactly the same as for our earlier servlets except that we're now using context and session attributes (for account balances and user transactions respectively) rather than instance variables.

We also re-write of the showData() method to take attribute values from the context and the session:

       private void showData(PrintWriter writer, HttpServletRequest req) {         Hashtable accounts = (Hashtable) servletContext.getAttribute("accounts");         if (accounts==null) {           return;         }         writer.println("<b>Account Balances</b><br><br>");         for (Enumeration keys = accounts.keys(); keys.hasMoreElements();) {           String thisKey = (String) keys.nextElement();           String thisBalance = (String) accounts.get(thisKey);           writer.println("Account " + thisKey + " has balance " +                          thisBalance + "<br>");         }         HttpSession session = req.getSession(true);         Vector userTransactions = (Vector) session.getAttribute("transactions");         if (userTransactions == null) {           return;         }         writer.println("<br><b>User Transactions</b><br><br>");         int i = userTransactions.size() - 1;         if (i >= 0) {           writer.println("<b>" + userTransactions.elementAt(i) + "</b><br>");         }         while (--i >= 0) {           writer.println("" + userTransactions.elementAt(i) + "<br>");         }       } 

We also have modified versions of the init() and resetValues() methods:

       public void init(ServletConfig config) throws ServletException {         super.init(config);         this.servletContext = config.getServletContext();         resetValues(null);       }       public void init() throws ServletException {}       public void resetValues(HttpServletRequest req) throws ServletException {         Hashtable accounts = (Hashtable) servletContext.getAttribute("accounts");         if (accounts == null) {           accounts = new Hashtable();         }         accounts.put("000001", "1000");         accounts.put("000002", "1000");         servletContext.setAttribute("accounts",accounts);         if (req == null) {           return;         }         HttpSession session = req.getSession(true);         Vector userTransactions = new Vector();         session.setAttribute("transactions", userTransactions);       }     } 

Using Servlets with Session and Context Attributes

To show the behavior we invoke the servlet, and reset it in two separate browser windows, by accessing http://localhost:8080/Synchronization/servlet/synchronization.SessionBankServlet?reset=true. In the first window we then access http://localhost:8080/Synchronization/servlet/synchronization.SessionBankServlet?from=000001&to=000002&amount=500 twice. The end result is a transfer of 1000 from account 000001 to account 000002:

click to expand

In the second browser window we invoke the same servlet by accessing http://localhost:8080/Synchronization/servlet/synchronization.SessionBankServlet?from=000002&to= 000001&amount=500. There are two things to note here. The account balances show (correctly) that we've transferred 500 back from account 000002 to account 000001. Also, the transaction list contains only one entry - the one we made in this window (on behalf of this user) and not the complete list of transactions made in all windows (by all users):

click to expand

We've succeeded in recording a list of transactions on a per-user basis by using the application's servlet context. No matter how many servlet instances are created (if we used SingleThreadModel and the container supported pooling, or if the servlet was part of a distributed application) these account balances will still be held in common.

To further understand what has happened in the container, we'll look at debug trace produced by the DebugListener from Chapter 10. If you want to record this kind of information you'll need to set up the Debugging Filter and Event Listener for this web application as set out in Chapter 10. You'll also need to make the following addition to web.xml:

     <web-app>       <filter>         <filter-name>DebugFilter</filter-name>         <filter-class>debugging.DebugFilter</filter-class>       </filter>       <filter-mapping>         <filter-name>DebugFilter</filter-name>         <url-pattern>/*</url-pattern>       </filter-mapping>       <listener>         <listener-class>debugging.DebugListener</listener-class>       </listener>       ...     </web-app> 

Inside the Container

Here is the debug trace for the sequence of interactions that we've just run through. The first block of three entries tells us that the account balances in context org.apahce.catalina.core.ApplicationContext@8238f4 have been set to 1500 and 500 as a result of a transfer recorded on session D466...765, which resulted from the request ?to=000002&from=000001&amount=500 on servlet SessionBankServlet:

click to expand

The second block of three entries tells us that the account balances in the same context have been set to 2000 and 0 as a result of a transfer transaction recorded on the same session, which resulted from the request ?to=000002&from=000001&amount=500 on the same servlet:

click to expand

The final block of three entries tells us that the account balances in the same context have been set to 1500 and 500 as a result of a transfer transaction recorded on a different session D00...371, which resulted from the request ?to=000001&from=000002&amount=500 on the same servlet:

click to expand

Testing the Race Condition

So far so good, but we were careful to invoke the servlets from the two windows in series. Let's look at what happens if we issue concurrent requests by accessing http://localhost:8080/Synchronization/servlet/synchronization.SessionBankServlet?from=000001&to= 000002&amount=500 simultaneously in the two browser windows. In the first browser window we see:

click to expand

And in the second browser window we see:

click to expand

This is exactly the same race condition as the one we encountered when using instance variables. Although the context and session both provide convenient data stores with predictable scope, access to the values in those data stores must still be synchronized by implementing SingleThreadModel. In our case we must explicitly synchronized the transfer() method once more:

     private synchronized void transfer(String fromAccount, String toAccount,                                        String amount) 

start sidebar

Multiple servlets executing request threads may have active access to a single session or context object at the same time, so the developer has the responsibility to synchronize access to these resources as appropriate.

end sidebar



 < Free Open Study > 



Professional Java Servlets 2.3
Professional Java Servlets 2.3
ISBN: 186100561X
EAN: 2147483647
Year: 2006
Pages: 130

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