前言
在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错误。