【Java】ThreadLocal 用法与原理

ThreadLocal是并发场景下的一种技术。主要用于为多线程环境中的共享变量提供线程级别的隔离,这样对于同一个变量,每一个线程都有自己的副本。

有一句话总结的很好,threadlocal是用空间换时间,而synchronized则是时间换空间。

有人说threadlocal与synchronized没有比较的意义,这里认为很有必要比较。因为二者都是为了解决多线程环境下访问共享数据时引发的一系列问题。多线程环境为什么会出现问题,归根结底就是因为有共享数据。synchronized是强迫每一线程串行的去处理数据,付出了时间代价;而threadlocal是为每一份线程保存一个数据的副本,付出了空间的代价。

常见用法:

一般我们要使用一个共享变量需要这样:

        ThreadLocal<C> threadLocal = new ThreadLocal<C>(){
            @Override
            protected C initialValue() {
                return c;
            }
        };

如果不覆盖initialValue()方法,那么默认的返回值是null。

ThreadLocal只是表明该变量是一个类型为C的共享变量,C才是真正的类型。(C由具体使用场景来定)。

这样每一个线程要访问该变量时,就可以使用threadLocal.get(),拿到自己的副本。别忘了threadlocal的设计理念是每一个线程一个副本,这个副本是存在每一个线程对象中的。

下面看下实现:

    protected T initialValue() {
        return null;
    }

默认值,不覆盖就返回null。

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
先取得当前线程,然后得到该线程的threadLocal的map。
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

如果map里有值,那么就直接返回,否则赋予一个默认值,再返回。

    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;
    }

这里调用了之前的initialValue方法。

从这里可以看到,每一个threadlocal变量都是存在thread对象的一个map里。这个map的key是threadlocal对象,value则是threadlocal对象定义时指定的泛型类型的对象,会由initialValue方法提供默认值。

这个map的类型在threadlocal类中定义:

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
实现了基本的map操作。

再看下threadlocal的set方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

这便是threadlocal的基本实现。下面需要明白threadlocal的使用场景。这个和最开始的时间-空间代价相关。threadlocal的使用场景很简单,就是每一个线程一个副本,足够简单。如果我们的需求也可以概括为上述特征,那就可以使用。相比较之下,sychronized的使用场景更加细化也更加复杂,比如并发环境下的初始化判断(单例模式),并发环境下的hashmap的设计等,通常是一种细粒度的控制。这个结合具体的使用场景还是很好区分的。

对于threadlocal,很常见的应用是web项目中的使用。java web底层是servlet,每一次请求都是一个线程,而且每一个servlet是单例的。所以我们可以定义一个threadlocal的变量用于跟踪每一个请求。

比如我们可以写一个静态类用于存储这个threadlocal变量,在servlet入口前写一个filter,用于把这次请求的相关信息赋值给threadlocal,那么在之后的一些列调用链中,都可以使用这个工具类获取当前请求的信息。我们也可以为当前请求自定义一些标记,根据一些判断为请求打上标记,特殊处理。举个例子:

这里使用原生的servlet api构建一个java web项目。

写一个包含threadlocal变量的工具类,记录请求的ip地址:

package interceptor;

public class RequestUtil {
    private ThreadLocal<Context> context = new ThreadLocal<Context>(){
        @Override
        protected Context initialValue() {
            return new Context();
        }
    };

    private RequestUtil(){

    }

    private static class InstanceHolder{
        private static RequestUtil instance = new RequestUtil();
    }

    private class Context{
        String ip;
    }


    public static RequestUtil getInstance(){
        return InstanceHolder.instance;
    }

    public static ThreadLocal<Context> getContext(){
        return getInstance().context;
    }

    public static void setIp(String ip){
        getContext().get().ip = ip;
    }

    public static String getIp(){
        return getContext().get().ip;
    }

}

写一个filter来为每一个请求设置ip地址:

package interceptor;

import javax.servlet.*;
import java.io.IOException;

public class RequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String ip = request.getRemoteAddr();
        RequestUtil.setIp(ip);
        chain.doFilter(request, response);
    }
}

在具体的servlet中使用:

package controller;

import interceptor.RequestUtil;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class MainController extends HttpServlet {

    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
            throws ServletException, IOException
    {
        PrintWriter out = response.getWriter();
        out.println("<h1>" + "hello" + "</h1>");

        System.out.println(RequestUtil.getIp());
    }

}

web.xml:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <filter>
    <filter-name>requestFilter</filter-name>
    <filter-class>interceptor.RequestFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>requestFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>controller.MainController</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/hello.do</url-pattern>
  </servlet-mapping>


</web-app>

这里的ip只是一个例子,我们可以根据自己具体的业务场景来指定context类。

还有一点需要特别注意:

threadlocal变量是线程隔离的,但是java web服务器通常会池化线程,所以一次请求可能由上一次的线程来处理,这时就会访问到上一次的threadlocal变量,所以我们需要在入口的filter中重新初始化threadlocal变量,以保证每一次得到的值都是新的,而不是上一次的旧值,当然这就与java web底层实现相关了。

所以我们可以在之前的Util类里面添加一个init方法:

    public static void init(){
        getContext().set(getInstance().genContext());
    }

    private Context genContext(){
        return new Context();
    }

然后再filter里调用一下init:

 @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        RequestUtil.init();

        String ip = request.getRemoteAddr();
        RequestUtil.setIp(ip);
        chain.doFilter(request, response);
    }


猜你喜欢

转载自blog.csdn.net/u010900754/article/details/80383037