线程安全的概念范畴:
线程安全,指的是在多线程环境下,一个类在执行某个方法时,对类的内部实例变量的访问是安全的。如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。对于下面的两种变量,不存在任何线程安全的说法:(1)方法签名中的任何参数变量(2)处于方法内部的局部变量。因为这两种变量都处于方法体的内部,由当前的执行线程独自管理。在传统的web开发中,处理http请求的最常用方式是通过实现Servlet对象来进行http请求的相应,servlet是J2EE的重要标准之一,规定了Java如何相应http请求的规范,通过HttpServletRequest和HttpServletResponse对象,我们能够轻松地与Web容器交互。
当web容器收到一个http请求时,Web容器中的一个主调度线程会从事先定义好的线程池中分配一个当前工作线程,将请求分配给当前的工作线程,由该线程来执行对应的Servlet对象中的service方法,当这个工作线程正在执行的时候,Web容器收到另外一个请求,主调度线程会同样从线程池中选择另一个工作线程来服务新的请求。Web容器本身并不关心这个新的请求是否访问的是同一个Servlet实例。即:对于同一个Servlet对象的多个请求,Servlet的service方法将在一个多线程的环境中并发执行。所以,Web容器默认采用单实例(单Servlet实例)多线程的方式来处理http请求。
这种处理方式可以减少新建Servlet实例的开销,从而缩短了对http请求的响应时间。但是,这样的处理方式会导致变量访问的线程安全问题,也就是说,Servlet对象并不是一个线程安全的对象。、
下面的例子很好的说明了这个问题。部分代码如下:
public class CurrentServlet extends HttpServlet {
PrintWriter out;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType ("text/html; charset=gb2312");
String username = request.getParameter("username");
out = response.getWriter();
try {
//为了突出并发的问题,设置延时
Thread.sleep(5000);
} catch (InterruptedException e) {}
out.println("用户名为:"+username);
}
}
这个例子中声明了一个实例变量out,并在doPost方法中将其赋值为用户的输出。当只有一个用户访问时,程序正常运行,如果多用户并发访问,可能就会出现某用户的信息显示给了其他用户的问题,这是十分严重的问题。加入5000ms延时是为了突出并发问题,可以分别打开两个网页,输入地址来进行访问:
用户a:http://localhost:8080/....../CurrentServlet?username=a
用户b:http://localhost:8080/....../CurrentServlet?username=b
最终结果:
可以看到Web服务器启动了两个线程分别处理用户a和b的请求,但是在用户a的浏览器上得到的是空白,而a的信息显示在了b的浏览器上,显然,servlet存在线程不安全问题。
Java的内存模型JMM主要规定了线程和内存之间的一些关系,系统存在一个主内存(Main Momery),Java中所有实例变量都存在主存中,对于所有线程也都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,缓存不可能总和主存同步,也就是说缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。本例的JMM示意图如下:
时间 | A线程 | B线程 |
T1 | 访问servlet页面 | |
T2 | 访问servlet页面 | |
T3 | out=a的输出,username=a,休眠5000ms,让出CPU | |
T4 | out=b的输出,username=b,休眠5000ms,让出CPU | |
T5 | 在用户b的浏览器上输出a线程username的值,线程结束 | |
T6 | 在用户b的浏览器上输出b线程username的值,线程结束 |
由于B线程对实例变量out的修改覆盖了a线程对实例变量out的修改,从而导致用户a的信息显示在了b的浏览器上。这说明,如果多用户并发大量访问时,是有出现问题的隐患的。
几种解决方法:
网上看到的解决方法有3种,各有利弊,简要带过,重点说ThreadLocal模式。
(1)实现SingleThreadModel接口
Servlet实现这个接口后,每个请求都会创建单独的servlet实例,就没有线程安全的问题了,但造成大量开销,已不提倡使用。
(2)使用synchronized关键字
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。当然,这个被用作“锁机制”的变量是多个线程共享的。提供一份变量,让不同的线程排队访问。(以时间换空间)
(3)避免使用实例变量
(4)使用ThreadLocal模式
java.lang.ThreadLocal,提供了一种解决多线程并发问题的方案,该类在维护变量时,实际使用了当前线程中的一个叫做ThreadLocalMap的独立副本,每个线程可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程,避免了线程访问实例变量发生冲突的问题。ThreadLocal本身并不是一个变量,而是通过操作当前线程的一个内部变量来达到与其他线程隔离的目的。从命名也可以看出操作的对象是线程的一个本地变量。Thread中:
public class Thread implements Runnable {
//省略
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
可以看到ThreadLocalMap跟随着当前的线程而存在,不同的线程Thread,拥有不同的ThreadLocalMap的本地实例变量。再看ThreadLocal类部分代码:
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
//省略
}
ThreadLocal变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocal变量,同时,ThreadLocal变量的值是在ThreadLocal对象进行set或者get操作时创建的。在创建ThreadLocalMap之前,会首先检查当前线程中的ThreadLocalMap变量是否已经存在,如果不存在创建一个,如果存在,使用当前线程已创建的ThreadLocalMap,使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal实例作为key进行存储。
这样就实现了数据访问的隔离,包括横向隔离和纵向隔离。横向——线程与线程之间的数据访问隔离,因为每个线程在进行对象访问时,访问的都是各个线程自己的ThreadLocalMap。纵向——同一个线程中,不同的ThreadLocal实例操作的对象之间相互隔离,因为ThreadLocalMap在存储时采用当前ThreadLocal的实例作为key来保证。
要完成ThreadLocal模式,最关键是要创建一个任何地方都可以访问到的ThreadLocal实例,将例子进行修改,增加一个类:
public class ValueThreadLocal {
private static ThreadLocal<PrintWriter> threadLocal = new ThreadLocal<PrintWriter>(){
// @Override
// protected synchronized PrintWriter initialValue(){
// return null;
// }
};
public static PrintWriter get(){
return threadLocal.get();
}
public static void set(PrintWriter out){
threadLocal.set(out);
}
}
实现了一个静态的ThreadLocal变量,通过set和get方法来操作ThreadLocal中存储的值。之后加入到之前的servlet中,修改为:
public class CurrentServlet extends HttpServlet {
PrintWriter out;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType ("text/html; charset=gb2312");
String username = request.getParameter("username");
//out = response.getWriter();
ValueThreadLocal.set(response.getWriter());
try {
//为了突出并发的问题,设置延时
Thread.sleep(5000);
} catch (InterruptedException e) {}
ValueThreadLocal.get().println("用户名为:"+username);
//out.println("用户名为:"+username);
}
}
这里通过 ValueThreadLocal.set(response.getWriter()); 以及 ValueThreadLocal.get().println("用户名为:"+username); 即操作了ThreadLocal中的值,再次运行浏览器中的两个测试,得到正确的结果:
实现ThreadLocal模式的两个主要步骤:
(1)建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境。
(2)在类中实现访问静态ThreadLocal变量的静态方法(set/get)