Servlet会话跟踪 - 会话属性的线程安全性

时间:2017-10-03 13:57:39

标签: multithreading session servlets thread-safety

美好的一天,所有。

我有一个servlet,我在其中声明,启动并分配给会话属性的局部变量。

我需要servlet是线程安全的,这样同一台计算机上的多个浏览器窗口可以让servlet运行,你可以在每个窗口中执行计算,但是这样各个线程中的操作不会相互影响,也就是说,帐户余额变量没有在线程之间共享,导致任何时候任何线程的状态都不一致。

以下是我的代码:

// Import servlet and HTTP functionality packages.
import javax.servlet.*;
import javax.servlet.http.*;

// Import packages to: handle user inputs and outputs, enable usage of decimal formatting of numbers.
import java.io.*;
import java.util.*;
import java.text.DecimalFormat;

public class SessionBank extends HttpServlet    // Define concrete class and extend HTTP functionality.
{
    public void init() throws ServletException  // Initialise variables at start of the servlet. Possible exception.
    {

    }

    // The method to output the initial HTML form to the screen, addressing also possible exceptions.
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
    {
        // Declare and initiate local variables.
        double balance = 0;                     // The current balance variable, un-formatted number, initiate to zero.
        String formattedBal = "";               // The current balance variable, formatted to show as currency amount, initially blank.

        // Set balance and formatted balance as session attributes.
        request.getSession().setAttribute("balance", balance);
        request.getSession().setAttribute("formattedBal", formattedBal);

        showBalance(request, response); // Call custom-defined method to display initial page.
    }

    // Method to respond to user's button inputs - output relevant HTML back to the screen. Possible exceptions.
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
    {
        HttpSession session = request.getSession(true);     // Establish session object.

        response.setContentType("text/html");       // Set response type to text/HTML.
        response.setHeader("Expires", "Tues, 01 Jan 1980 00:00:00 GMT");    // Set past date to forbid cache.

        // If user clicks "Deposit" button:
        if(request.getParameter("depButton") != null && request.getParameter("depButton").equals("Deposit"))
        {
            if(verifyAmount(request))   // If entered amount passes verification.
            {
                addToBalance(request, session);         // Add the amount to current balance.
                redisplayCurrentPage(response);     // Redisplay initial page.
            }
            else        // If entered amount does not pass verification.
            {
                showErrorPage(response);    // Display error page.
            }
        }

        // If user clicks "Withdraw" button:
        if(request.getParameter("withdrButton") != null && request.getParameter("withdrButton").equals("Withdraw"))
        {
            if(verifyAmount(request))   // If entered amount passes verification.
            {
                subtractFromBalance(request, session);  // Subtract the amount from current balance.
                redisplayCurrentPage(response);     // Redisplay initial page.
            }
            else        // If entered amount does not pass verification.
            {
                showErrorPage(response);    // Display error page.
            }
        }

        // If user clicks "Balance" button:
        if(request.getParameter("balButton") != null && request.getParameter("balButton").equals("Balance"))
        {
            showBalance(request, response);     // Display current formatted balance on page.
        }
    }

    private boolean verifyAmount(HttpServletRequest request)    // Method to verify entered amount, based on textbook criteria.
    {
        boolean amountValid = false;    // Declare and initiate a validity variable.

        // If entered amount is not blank and is greater than zero, return validity as true. Else, return false.
        if(request.getParameter("amount") != "" && Double.parseDouble(request.getParameter("amount")) > 0)
            amountValid = true;
        else
            amountValid = false;

        return amountValid;     // Return validity variable.
    }

    // Method to add amount to balance, addressing possible exception.
    private void addToBalance(HttpServletRequest request, HttpSession session) throws IOException
    {
        double userAmount = Double.parseDouble(request.getParameter("amount")); // Declare and assign entered amount variable.

        // Down-cast session attribute object to String, then parse into double type variable.
        double balOld = Double.parseDouble(String.valueOf(session.getAttribute("balance")));

        double balNew = balOld + userAmount;        // Add the amount to current balance and save the value.

        session.setAttribute("balance", balNew);    // Assign new balance to the session attribute.
    }

    // Method to subtract from balance. Possible exception.
    private void subtractFromBalance(HttpServletRequest request, HttpSession session) throws IOException
    {
        double userAmount = Double.parseDouble(request.getParameter("amount")); // Declare and assign entered amount value.

        // Down-cast session attribute object to String, then parse into a double type variable.
        double balOld = Double.parseDouble(String.valueOf(session.getAttribute("balance")));

        double balNew = balOld - userAmount;        // Subtract the amount from the balance and save the value.

        session.setAttribute("balance", balNew);    // Assign new balance value to the session attribute.
    }

    private void showBalance(HttpServletRequest request, HttpServletResponse response) throws IOException   // Method to output balance HTML page. Possible exception.
    {
        PrintWriter out = response.getWriter();     // Establish HTML writer object.

        formatBalance(request); // Format current balance for displaying.

        out.println("<html>");
            out.println("<hr>");        // Horizontal line.
            out.println("<title>Online Bank ATM Simulator</title>");        // Title to show on browser title bar.
            out.println("<h1 align = \"center\">Bank ATM Simulation</h1>"); // Page heading, centered on page.
            out.println("<body onLoad = \"amount.focus()\">");              // Set focus to the text-field.
                out.println("<form method = \"POST\" action = \"../servlet/SessionBank\">");    // Form method and submission address.
                    out.println("<center>");        // Tag to center the following output on page.
                    out.println("Amount: ");
                    out.println("<input type = \"text\" name = \"amount\" id = \"amount\" size = \"20\"><br><br>"); // Amount text field.
                    out.println("Balance: ");
                    out.println(request.getSession().getAttribute("formattedBal") + "<br><br>");    // Current formatted balance shown.
                    out.println("<button name = \"balButton\" value = \"Balance\">Balance</button>");   // "Balance" button.
                    out.println("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");  // Spacers.
                    out.println("<button name = \"depButton\" value = \"Deposit\">Deposit</button>");   // "Deposit" button.
                    out.println("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");  // Spacers.
                    out.println("<button name = \"withdrButton\" value = \"Withdraw\">Withdraw</button>");  // "Withdraw" button.
                    out.println("</center>");       // Tag to end centering of output on page.
                out.println("</form>");     // End of form.
            out.println("</body>");
            out.println("<br>");
            out.println("<hr>");        // Horizontal line.
        out.println("</html>");
    }

    // Method to redisplay form after deposit/withdrawal.
    private void redisplayCurrentPage(HttpServletResponse response) throws IOException
    {
        PrintWriter out = response.getWriter();     // Establish HTML writer object.

        out.println("<html>");
            out.println("<hr>");        // Horizontal line.
            out.println("<title>Online Bank ATM Simulator</title>");        // Title to show on browser title bar.
            out.println("<h1 align = \"center\">Bank ATM Simulation</h1>"); // Page heading, centered on page.
            out.println("<body onLoad = \"amount.focus()\">");              // Set focus to the text-field.
                out.println("<form method = \"POST\" action = \"../servlet/SessionBank\">");    // Form method and submission address.
                    out.println("<center>");        // Tag to center the following output on page.
                    out.println("Amount: ");
                    out.println("<input type = \"text\" name = \"amount\" id = \"amount\" size = \"20\"><br><br>"); // Amount text field.
                    out.println("Balance: ");
                    out.println("<br><br>");    // No formatted balance value shown.
                    out.println("<button name = \"balButton\" value = \"Balance\">Balance</button>");   // "Balance" button.
                    out.println("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");  // Spacers.
                    out.println("<button name = \"depButton\" value = \"Deposit\">Deposit</button>");   // "Deposit" button.
                    out.println("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");  // Spacers.
                    out.println("<button name = \"withdrButton\" value = \"Withdraw\">Withdraw</button>");  // "Withdraw" button.
                    out.println("</center>");       // Tag to end centering of output on page.
                out.println("</form>");     // End of form.
            out.println("</body>");
            out.println("<br>");
            out.println("<hr>");        // Horizontal line.
        out.println("</html>");
    }

    private void formatBalance(HttpServletRequest request)  // Method to format the current balance number to a currency amount.
    {
        DecimalFormat dollars = new DecimalFormat("$###,###.###");  // Construct new decimal format.

        // Down-cast session attribute to String, parse to double, then format using the above decimal format and save value.
        String formattedBal = dollars.format(Double.parseDouble(String.valueOf(request.getSession().getAttribute("balance"))));

        request.getSession().setAttribute("formattedBal", formattedBal);    // Assign new formatted balance to session attribute.
    }

    // Method to output error HTML page, if entered amount does not pass verification. Possible exception.
    private void showErrorPage(HttpServletResponse response) throws IOException
    {
        PrintWriter out = response.getWriter();     // Establish HTML writer object.

        out.println("<html>");
            out.println("<head>");
                out.println("<title>Amount Input Error</title>");   // Title to show in browser title bar.
            out.println("</head>");

            out.println("<body>");
                out.println("<h1>Error processing the input.</h1><br>");    // Heading text.
                out.println("Please ensure your input:<br><br>");
                out.println("- Is not blank.<br>");
                out.println("- Is strictly a number.<br>");
                out.println("- Is a positive, non-zero amount.");
            out.println("</body>");
        out.println("</html>");
    }

    public void destroy()   // Method to terminate the servlet.
    {

    }
}

现在,当使用多个线程(运行servlet的浏览器窗口)时,一个线程覆盖的余额成为另一个线程读取的余额。这当然是不正确的。我需要每个并发线程都有自己的变量副本以获得一致的结果。

据我所知,与将余额存储为类(实例)变量不同,将其存储为本地并分配给会话属性是线程安全的。但如果是这样,为什么线程会更新彼此的变量?什么编码不正确?

非常感谢!

1 个答案:

答案 0 :(得分:0)

浏览器可能与此有点不一致,但最终您看到的是单个浏览器与您的服务器通过单个会话cookie(通常名为JSESSIONID)进行交互。从服务器和代码的角度来看,由于单个cookie,它不知道有多个选项卡或浏览器。

在这种情况下,一个线程访问会话中的值,更新并存储它。另一个线程 - 为不同的浏览器选项卡提供服务 - 做同样的事情。但是会话中只有一个会话和一个变量,因此它最终在浏览器选项卡之间共享。

以跨平台的方式解决这个问题可能是一个挑战。一种方法是在浏览器中使用一些JavaScript在启动时生成唯一的数字或代码。因此,举例来说,您可以在HTML代码中创建onload方法,并使用简单的Math.random() JavaScript调用。然后,对于每个请求,您将此数字传递给后端。在后端,您可以创建一个Map,将JavaScript生成的数字映射到另一个参数Map并将其存储在会话中。

您的代码使实现起来有点困难,因为您在一个地方同时拥有显示和后端逻辑(通常它将是显示器的JSP和后端的servlet)但它可能是完成。