Java学习整理系列之ThreadLocal的理解

ThreadLocal概念

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

ThreadLocal与Connection

疑问1:有一个用户请求就会启动一个线程。而如果ThreadLocal用的是变量副本,那我们把connection放在Threadlocal里的话,那么我们的程序只需要一个connection连接数据库就行了,每个线程都是用的connection的一个副本,那为什么还有必要要数据库连接池呢?

ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。

疑问2:既然ThreadLocal当当前线程中没有时去新建一个新的,有的话就用当前线程中的,那数据库连接池已经有了这种功能啊,还要ThreadLocal干什么?

由于请求中的一个事务涉及多个 DAO 操作,而这些 DAO 中的 Connection 不能从连接池中获得,如果是从连接池获得的话,两个 DAO 就用到了两个Connection,这样的话是没有办法完成一个事务的。DAO 中的 Connection 如果是从 ThreadLocal 中获得 Connection 的话那么这些 DAO 就会被纳入到同一个 Connection 之下。当然了,这样的话,DAO 中就不能把 Connection 给关了,关掉的话,下一个使用者就不能用了。


ThreadLocal与同步机制

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本(每个线程创建一个,不是什么对象的拷贝或副本),从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。


框架中使用ThreadLocal解决线程安全问题

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因此有状态的Bean就可以在多线程中共享了。一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

我们在使用Hibernate的时候,经常会使用到currentSession(),而Hibernat是这样使用ThreadLocal的。

public static final ThreadLocal session = new ThreadLocal();   
    public static Session currentSession() {
        Session s = (Session)session.get(); 
        // open a new session,if this session has none  
        if(s == null){   
              s = sessionFactory.openSession(); 
              session.set(s);   
        }   
          return s;   
    }   

 
  (1)初始化一个ThreadLocal对象,ThreadLocal有三个成员方法 get()、set()、initialvalue()。 如果不初始化initialvalue,则initialvalue返回null。(2)session的get根据当前线程返回其对应的线程内部变量,也就是我们需要的net.sf.hibernate.Session(相当于对应每个数据库连接).多线程情况下共享数据库链接是不安全的。ThreadLocal保证了每个线程都有自己的s(数据库连接)。(3)如果是该线程初次访问,自然,s(数据库连接)会是null,接着创建一个Session,具体就是行6。 (4)创建一个数据库连接实例 s (5)保存该数据库连接s到ThreadLocal中。 (6)如果当前线程已经访问过数据库了,则从session中get()就可以获取该线程上次获取过的连接实例。 
  

在自己的项目中使用ThreadLocal解决线程安全问题

这里已request对象为例,首先我们新建一个常量类用来新建ThreadLocal对象,代码如下:

public class CommonConstant {
	public static final ThreadLocal requestTL = new ThreadLocal(); // 保存request的threadlocal
}

接着新建一个Request工具类,用来获得当前的request或取得request里面的属性等,代码如下:

public class RequestUtil {
	public static Object getAttribute(String name) {
		return CommonConstant.requestTL.get().getAttribute(name);
	}
	public static void setAttribute(String name, Object value) {
		CommonConstant.requestTL.get().setAttribute(name, value);
	}
	public static void removeAttribute(String name) {
		CommonConstant.requestTL.get().removeAttribute(name);
	}
	public static boolean containsKey(String name) {
		Object value = getAttribute(name);
		if (value != null) {
			return true;
		}
		return false;
	}
	public static boolean notContainsKey(String name) {
		Object value = getAttribute(name);
		if (value == null) {
			return true;
		}
		return false;
	}
	public static HttpServletRequest getRequest() {
		return CommonConstant.requestTL.get();
	}
	public static String getParameter(String name) {
		return CommonConstant.requestTL.get().getParameter(name);
	}
}

接着最重要的就是什么时候将这个request放入当前线程,比如在Servlet中当然可以在dosomething(HttpServletRequest request, HttpServletResponse response){}这样的方法里,当然也可以再一些拦截器,过滤器的时候进行设置,如Spring的preHandle(HttpServletRequest request, HttpServletResponse response, Object handler),代码如下:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		CommonConstant.requestTL.set(request);
	}

那么接下来在需要的地方如Service层中就可以这样获取到当前线程的request了,代码如下:

RequestUtil.getRequest();
RequestUtil.getAttribute(name);
RequestUtil.getParameter(name);
//如果没有RequestUtil,那么就先取得request
HttpServletRequest request = CommonConstant.requestTL.get();

ThreadLocal简单实现

大概思路就是ThreadLocal对象中有一个map,map中保存的键值对的key是当前线程,值是线程局部变量的值。

public class ThreadLocal { 
    private Map values = Collections.synchronizedMap(new HashMap());

    public Object get() {
        Thread curThread = Thread.currentThread();
        Object o = values.get(curThread);
        if (o == null && !values.containsKey(curThread)) {
            o = initialValue();
            values.put(curThread, o);
        }
        return o;
    }

    public void set(Object newValue) {
        values.put(Thread.currentThread(), newValue);
    }

    public Object initialValue() {
        return null;
    }
}
这个实现和JDK的总体思路类似,但存在很多问题。因为用 Thread 对象做 values 映射表中的key将导致无法在线程退出后对 Thread 进行垃圾回收,而且也无法存储多个线程局部变量。从JDK5.0的源码来看,并非在ThreadLocal中有一个Map,而是在每个Thread中存在这样一个Map,具体是ThreadLocal.ThreadLocalMap。当用set时候,往当前线程里面的Map里 put 的key是当前的ThreadLocal对象。而不是把当前Thread作为Key值put到ThreadLocal中的Map里。 

ThreadLocal源代码实现

首先我们来看看JDK中的源码,部分源码如下:

public class ThreadLocal {
    protected T initialValue() {
        return null;
    }
    public ThreadLocal() {
    }
    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();
    }
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    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 void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

从public void set(T value){}可以看出在设置当前线程的线程局部变量时,首先获得当前的线程,接着获取一个与当前线程关联的ThreadLocalMap对象(ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取,可以用常用的Map对象来理解),如果该ThreadLocalMap对象存在,则将当前线程局部变量和其值放进map,否则的话新建一个map,并使线程的threadLocals为新的map的引用。而当我们通过public T get(){}获取当前线程的线程局部变量时,如果当前线程有相应的ThreadLocalMap对象,则已线程局部变量为键,取出其值,如果map不存在,则通过setInitialValue();将初始化值也就是null设置到map中,并返回该初始化值。

这里我想强调的是每一个Thread对象中有一个ThreadLocal.ThreadLocalMap threadLocals对象引用,指向的是一个ThreadLocalMap对象,该map用来存储一些键值对,如;,键值对中前一个为定义的线程局部变量,后一个为具体保存的变量。

public static final ThreadLocal requestTL = new ThreadLocal(); // 保存request的threadlocal
public static final ThreadLocal responseTL = new ThreadLocal(); // 保存response的threadlocal







你可能感兴趣的:(Java)