Java多线程编程核心技术(第3版)
上QQ阅读APP看书,第一时间看更新

1.2.9 Servlet技术也会引起“非线程安全”问题

非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序执行流程。下面通过一个示例来学习如何解决非线程安全问题。

创建t4_threadsafe项目,以实现非线程安全的环境,LoginServlet.java代码如下:


package controller;

// 本类模拟成一个Servlet组件
public class LoginServlet {

private static String usernameRef;
private static String passwordRef;

public static void doPost(String username, String password) {
    try {
        usernameRef = username;
        if (username.equals("a")) {
            Thread.sleep(5000);
        }
        passwordRef = password;

        System.out.println("username=" + usernameRef + " password="
            + password);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
    }
}

}

线程ALogin.java代码如下:


package extthread;

import controller.LoginServlet;

public class ALogin extends Thread {
@Override
public void run() {
    LoginServlet.doPost("a", "aa");
}
}

线程BLogin.java代码如下:


package extthread;

import controller.LoginServlet;

public class BLogin extends Thread {
@Override
public void run() {
    LoginServlet.doPost("b", "bb");
}
}

运行类Run.java代码如下:


public class Run {

public static void main(String[] args) {
    ALogin a = new ALogin();
    a.start();
    BLogin b = new BLogin();
    b.start();
}

}

程序运行结果如图1-24所示。

运行结果是错误的,在研究问题的原因之前,首先要知道两个线程向同一个对象的public static void doPost(String username, String password)方法传递参数时,方法的参数值不会被覆盖,而是绑定到当前执行线程上。

图1-24 线程非安全

执行错误结果的过程如下。

1)在执行main()方法时,执行的结构顺序如下:


ALogin a = new ALogin();
a.start();
BLogin b = new BLogin();
b.start();

这样的代码顺序被执行时,很大概率会使ALogin线程先执行,而BLogin线程后执行,因为ALogin线程是首先执行start()方法的,并且在执行a.start()之后又执行了BLogin b = new BLogin(),实例化代码是需要耗时,更增加了ALogin线程先执行的概率。

2)ALogin线程首先执行了public static void doPost(String username, String password)方法,对username和password传入值a和aa。

3)ALogin线程执行usernameRef= username语句,将a赋值给usernameRef。

4)ALogin线程执行if(username.equals("a"))代码符合条件,执行Thread.sleep(5000)停止运行5秒。

5)BLogin线程也执行public static void doPost(String username, String password)方法,对username和password传入值b和bb。

6)由于LoginServlet .java是单例的,并且变量usernameRef和passwordRef使用static进行修饰,系统中只存在一份usernameRef和passwordRef变量,所以ALogin线程对usernameRef赋的a值被BLogin线程的b值所覆盖,usernameRef值变成b。

7)BLogin线程执行if(username.equals("a"))代码不符合条件,不执行Thread.sleep(5000),而继续执行后面的赋值语句,将passwordRef值变成bb。

8)BLogin线程执行输出语句,输出了b和bb的值。

9)5s之后,ALogin线程继续向下运行,注意,参数password的值aa是绑定到当前线程的,也就是ALogin线程,所以不会被BLogin线程的值bb所覆盖。将ALogin线程password的值aa赋值给变量passwordRef,而usernameRef还是BLogin线程赋的值b。

10)ALogin线程执行输出语句,输出了b和aa的值。

这就是对运行过程的分析。上面错误的结果也通过10个步骤进行了分析。

另外,去掉if和sleep语句后,如果BLogin线程得到优先执行的机会,那么输出的结果可能有两种:


b bb
a aa

a bb
a aa

但需要注意的是,如果代码改成如下所示:


ALogin a = new ALogin();
BLogin b = new BLogin();
a.start();
b.start();

那么输出的结果可能有以下两种:


a bb
a aa

b bb
b aa

解决这个非线程安全问题也是使用synchronized关键字,更改代码如下:


synchronized public static void doPost(String username, String password) {
    try {
        usernameRef = username;
        if (username.equals("a")) {
            Thread.sleep(5000);
        }
        passwordRef = password;

        System.out.println("username=" + usernameRef + " password="
                + password);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

加入synchronized关键字的方法可以保证同一时间只有一个线程在执行方法,多个线程执行方法具有排队的特性。

图1-25 排队进入方法,线程安全了

程序运行结果如图1-25所示。

在Web开发中,Servlet对象本身就是单例的,所以为了不出现的非线程安全,建议不要在Servlet中出现实例变量。