ThreadLocal的原理与使用

前言

  在java web项目中,经常会使用到单例对象,从服务器启动那一时刻就实例化全局对象。然后会对某些全局对象的属性进行修改之类的操作,但是我们知道项目一般都是部署到tomcat、Jboss之类的服务器上。浏览器的每个请求就是一个新的线程,这样如果  对全局对象的属性进行修改并使用,很可能就会造成数据不一致的错误问题。那怎么保证各自线程能正确使用自己修改过的共享变量呢?这时让我们想到ThreadLocal,那ThreadLocal是什么,为何能有如此神奇的行为呢?带着这个问题我们直接进入主题。

什么是ThreadLocal?

  ThreadLocal是java.lang包下面的一个类。见名知意,局部的线程。它能避免发生多线程对共享变量修改造成的数据错误问题!

ThreadLocal的底层原理

  ThreadLocal能使变量值和线程对象关联起来,保证线程封闭。下面是ThreadLocal类主要的方法:

1 public T get();
2 public void set(T value);
3 public void remove();
4 private T setInitialValue();

  get方法是获取保存在ThreadLocal中当前线程设置的共享变量副本值;

  set方法是用来设置当前线程的共享变量副本;

  set方法

1 public void set(T value) {
2         Thread t = Thread.currentThread();
3         ThreadLocalMap map = getMap(t);
4         if (map != null)
5             map.set(this, value);
6         else
7             createMap(t, value);
8     }

  set方法会获取当前线程(线程中有一个属性threadLocals,该属性属于ThreadLocal.ThreadLocalMap类)。如果当前线程的ThreadLocalMap对象不为空,直接把修改的值存放到ThreadLocalMap中;如果为空,则先实例化ThreadLocalMap对象,再存值。

ThreadLocalMap和HashMap相似,也是通过哈希表的数据结构来保存数据(数组加链表)。从set方法可以知道,一个线程有且只会创建一个ThreadLocalMap对象,线程会把修改后的变量值(变量副本)保存到ThreadLocalMap中。

void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}
        

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
       table = new Entry[INITIAL_CAPACITY];
       int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY -1);
       table[i] = new Entry(firstKey, firstValue);
       size = 1;
       setThreshold(INITIAL_CAPACITY);
}

  get方法

 1 public T get() {
 2         Thread t = Thread.currentThread();
 3         ThreadLocalMap map = getMap(t);
 4         if (map != null) {
 5             ThreadLocalMap.Entry e = map.getEntry(this);
 6             if (e != null)
 7                 return (T)e.value;
 8         }
 9         return setInitialValue();
10 }

  get方法会获取到当前线程ThreadLocalMap对象中保存的副本值。

注意:同一个ThreadLocal对象下,保存在ThreadLocalMap中的Entry对象下标相同。因为下标计算=threadLocalHashCode & (len-1); threadLocalHashCode 和len都是相同的。

  remove方法

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
 }

  获取当前线程的ThreadLocalMap对象,对象不为空,则调用remove(this);看看这个方法做了什么。

private void remove(ThreadLocal key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear(); 
                    expungeStaleEntry(i); // 清空Entry[] 数组对应下标的对象
                    return;
                }
            }
        }

  remove的目的是帮助GC清除多余对象,避免造成内存溢出。

实际项目中的运用

  项目中有一个全局对象HttpJsonResource,服务启动的时候初始化。初始化后会维护一个HttpJsonService对象到属性中,通过get方法能获取到HttpJsonService对象。看代码分析:

HttpJsonService service = getHttpJsonService(context, JYLS_URL);
String originUrl = service.getHttpURL();
// HttpJsonService 中的httpURL是个全局变量,originUrl是公用的套接字 

String newUrl = originUrl + this.urlSuffix; // urlSuffix是不同的服务编码
.....
使用HttpURLConnection调用其它服务;
HttpURLConnection conn;
URL url = new URL(originUrl + threadLocalURL.get()); conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(super.getReqMethod().toUpperCase()); conn.setRequestProperty("Connection", "close"); conn.setDoInput(true); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/json;charset=utf-8");//设置参数类型是json格式
// 启动了一个线程 HttpCommProcessThread task = new HttpCommProcessThread(this, conn, reqData, super.getReqMethod()); task.startUp();
// 阻塞主线程 task.waitForData(timeOut);
.
.
// 最后把HttpURL设置为原始的公用值
service.setHttpURL(originUrl);

  代码逻辑主要是获取到一个公用的http请求地址httpURL,然后拼接上服务编码,组成一个新的http请求去调用外部服务,调用完之后又把httpURL修改为原始值。看上去是OK的!但是有一个问题,调用接口比如超时了,在这一段时间内,A请求线程还被阻塞,又有一个请求B来调用,这时上A请求没来得及把httpURL修改为原始值,B请求又在A请求修改为新的url基础上进行拼接,导致utl错误。

posted @ 2019-11-14 16:44  小栗俊  阅读(290)  评论(0编辑  收藏  举报