Java中ThreadLocal无锁化线程封闭实现原理

虽然现在可以说很多程序员会用ThreadLocal,但是我相信大多数程序员还不知道ThreadLocal,而使用ThreadLocal的程序员大多只是知道其然而不知其所以然,因此,使用ThreadLocal的程序员很多时候会被它导入到陷进中去,其实java很多高级机制系列的很多东西都是一把双刃剑,也就是有利必有其弊,那么我们的方法是找到利和弊的中间平衡点,最佳的方式去解决问题。

本文首先说明ThreadLocal能做什么,然后根据功能为什么要用它,如何使用它,最后通过内部说明讲解他的坑在哪里,使用的人应该如何避免坑。

ThreadLocal的定义和用途的概述(我的理解):

它是一个线程级别变量,在并发模式下是绝对安全的变量,也是线程封闭的一种标准用法(除了局部变量外),即使你将它定义为static,它也是线程安全的。

ThreadLocal能做什么呢?

这个一句话不好说,我们不如来看看实际项目中遇到的一些困解:当你在项目中根据一些参数调用进入一些方法,然后方法再调用方法,进而跨对象调用方法,很多层次,这些方法可能都会用到一些相似的参数,例如,A中需要参数a、b、c,A调用B后,B中需要b、c参数,而B调用C方法需要a、b参数,此时不得不将所有的参数全部传递给B,以此类推,若有很多方法的调用,此时的参数就会越来越繁杂,另外,当程序需要增加参数的时候,此时需要对相关的方法逐个增加参数,是的,很麻烦,相信你也遇到过,这也是在C语言面向对象过来的一些常见处理手段,不过我们简单的处理方法是将它包装成对象传递进去,通过增加对象的属性就可以解决这个问题,不过对象通常是有意义的,所以有些时候简单的对象包装增加一些扩展不相关的属性会使得我们class的定义变得十分的奇怪,所以在这些情况下我们在架构这类复杂的程序的时候,我们通过使用一些类似于Scope的作用域的类来处理,名称和使用起来都会比较通用,类似web应用中会有context、session、request、page等级别的scope,而ThreadLocal也可以解决这类问题,只是他并不是很适合解决这类问题,它面对这些问题通常是初期并没有按照scope以及对象的方式传递,认为不会增加参数,当增加参数时,发现要改很多地方的地方,为了不破坏代码的结构,也有可能参数已经太多,已经使得方法的代码可读性降低,增加ThreadLocal来处理,例如,一个方法调用另一个方法时传入了8个参数,通过逐层调用到第N个方法,传入了其中一个参数,此时最后一个方法需要增加一个参数,第一个方法变成9个参数是自然的,但是这个时候,相关的方法都会受到牵连,使得代码变得臃肿不堪。

上面提及到了ThreadLocal一种亡羊补牢的用途,不过也不是特别推荐使用的方式,它还有一些类似的方式用来使用,就是在框架级别有很多动态调用,调用过程中需要满足一些协议,虽然协议我们会尽量的通用,而很多扩展的参数在定义协议时是不容易考虑完全的以及版本也是随时在升级的,但是在框架扩展时也需要满足接口的通用性和向下兼容,而一些扩展的内容我们就需要ThreadLocal来做方便简单的支持。

简单来说,ThreadLocal是将一些复杂的系统扩展变成了简单定义,使得相关参数牵连的部分变得非常容易,以下是我们例子说明:

Spring的事务管理器中,对数据源获取的Connection放入了ThreadLocal中,程序执行完后由ThreadLocal中获取connection然后做commit和rollback,使用中,要保证程序通过DataSource获取的connection就是从spring中获取的,为什么要做这样的操作呢,因为业务代码完全由应用程序来决定,而框架不能要求业务代码如何去编写,否则就失去了框架不让业务代码去管理connection的好处了,此时业务代码被切入后,spring不会向业务代码区传入一个connection,它必须保存在一个地方,当底层通过ibatis、spring jdbc等框架获取同一个datasource的connection的时候,就会调用按照spring约定的规则去获取,由于执行过程都是在同一个线程中处理,从而获取到相同的connection,以保证commit、rollback以及业务操作过程中,使用的connection是同一个,因为只有同一个conneciton才能保证事务,否则数据库本身也是不支持的。

其实在很多并发编程的应用中,ThreadLocal起着很重要的重要,它不加锁,非常轻松的将线程封闭做得天衣无缝,又不会像局部变量那样每次需要从新分配空间,很多空间由于是线程安全,所以,可以反复利用线程私有的缓冲区。

如何使用ThreadLocal?

在系统中任意一个适合的位置定义个 ThreadLocal 变量,可以定义为 public static 类型(直接new出来一个ThreadLocal对象),要向里面放入数据就使用set(Object),要获取数据就用get()操作,删除元素就用remove(),其余的方法是非 public 的方法,不推荐使用。

下面是一个简单例子(代码片段1):

public class ThreadLocalTest2 {

	public final static ThreadLocal <String>TEST_THREAD_NAME_LOCAL = new ThreadLocal<String>();

	public final static ThreadLocal <String>TEST_THREAD_VALUE_LOCAL = new ThreadLocal<String>();

	public static void main(String[]args) {
		for(int i = 0 ; i < 100 ; i++) {
			final String name = "线程-【" + i + "】";
			final String value =  String.valueOf(i);
			new Thread() {
				public void run() {
					try {
						TEST_THREAD_NAME_LOCAL.set(name);
						TEST_THREAD_VALUE_LOCAL.set(value);
						callA();
					}finally {
						TEST_THREAD_NAME_LOCAL.remove();
						TEST_THREAD_VALUE_LOCAL.remove();
					}
				}
			}.start();
		}
	}

	public static void callA() {
		callB();
	}

	public static void callB() {
		new ThreadLocalTest2().callC();
	}

	public void callC() {
		callD();
	}

	public void callD() {
		System.out.println(TEST_THREAD_NAME_LOCAL.get() + "/t=/t" + TEST_THREAD_VALUE_LOCAL.get());
	}
}

这里模拟了100个线程去访问分别设置 name 和 value ,中间故意将 name 和 value 的值设置成一样,看是否会存在并发的问题,通过输出可以看出,线程输出并不是按照顺序输出,说明是并行执行的,而线程 name 和 value 是可以对应起来的,中间通过多个方法的调用,以模实际的调用中参数不传递,如何获取到对应的变量的过程,不过实际的系统中往往会跨类,这里仅仅在一个类中模拟,其实跨类也是一样的结果,大家可以自己去模拟就可以。

相信看到这里,很多程序员都对 ThreadLocal 的原理深有兴趣,看看它是如何做到的,尽然参数不传递,又可以像局部变量一样使用它,的确是蛮神奇的,其实看看就知道是一种设置方式,看到名称应该是是和Thread相关,那么废话少说,来看看它的源码吧,既然我们用得最多的是set、get和remove,那么就从set下手:

set(T obj)方法为(代码片段2):

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

首先获取了当前的线程,和猜测一样,然后有个 getMap 方法,传入了当前线程,我们先可以理解这个map是和线程相关的map,接下来如果   不为空,就做set操作,你跟踪进去会发现,这个和HashMap的put操作类似,也就是向map中写入了一条数据,如果为空,则调用createMap方法,进去后,看看( 代码片段3 ):

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

返现创建了一个ThreadLocalMap,并且将传入的参数和当前ThreadLocal作为K-V结构写入进去( 代码片段4 ):

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

这里就不说明ThreadLocalMap的结构细节,只需要知道它的实现和HashMap类似,只是很多方法没有,也没有implements Map,因为它并不想让你通过某些方式(例如反射)获取到一个Map对他进一步操作,它是一个ThreadLocal里面的一个static内部类,default类型,仅仅在java.lang下面的类可以引用到它,所以你可以想到Thread可以引用到它。

我们再回过头来看看getMap方法,因为上面我仅仅知道获取的Map是和线程相关的,而通过 代码片段3 ,有一个t.threadLocalMap = new ThreadLocalMap(this, firstValue)的时候,相信你应该大概有点明白,这个变量应该来自Thread里面,我们根据getMap方法进去看看:

ThreadLocalMap getMap(Thread t) {
	return t.threadLocals;
}

是的,是来自于Thread,而这个Thread正好又是当前线程,那么进去看看定义就是:

ThreadLocal.ThreadLocalMap threadLocals = null;

这个属性就是在Thread类中,也就是每个Thread默认都有一个ThreadLocalMap,用于存放线程级别的局部变量,通常你无法为他赋值,因为这样的赋值通常是不安全的。

好像是不是有点乱,不着急,我们回头先摸索下思路:

1、Thread里面有个属性是一个类似于HashMap一样的东西,只是它的名字叫ThreadLocalMap,这个属性是default类型的,因此同一个package下面所有的类都可以引用到,因为是Thread的局部变量,所以每个线程都有一个自己单独的Map,相互之间是不冲突的,所以即使将ThreadLocal定义为static线程之间也不会冲突。

2、ThreadLocal和Thread是在同一个package下面,可以引用到这个类,可以对他做操作,此时ThreadLocal每定义一个,用this作为Key,你传入的值作为value,而this就是你定义的ThreadLocal,所以不同的ThreadLocal变量,都使用set,相互之间的数据不会冲突,因为他们的Key是不同的,当然同一个ThreadLocal做两次set操作后,会以最后一次为准。

3、综上所述,在线程之间并行,ThreadLocal可以像局部变量一样使用,且线程安全,且不同的ThreadLocal变量之间的数据毫无冲突。

我们继续看看get方法和remove方法,其实就简单了:

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

通过根据当前线程调用getMap方法,也就是调用了t.threadLocalMap,然后在map中查找,注意Map中找到的是Entry,也就是K-V基本结构,因为你set写入的仅仅有值,所以,它会设置一个e.value来返回你写入的值,因为Key就是ThreadLocal本身。你可以看到map.getEntry也是通过this来获取的。

同样remove方法为:

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

同样根据当前线程获取map,如果不为空,则remove,通过this来remove。

补充下(2013-6-29),搞忘写有什么坑了,这个ThreadLocal有啥坑呢,大家从前面应该可以看出来,这个ThreadLocal相关的对象是被绑定到一个Map中的,而这个Map是Thread线程的中的一个属性,那么就有一个问题是,如果你不自己remove的话或者说如果你自己的程序中不知道什么时候去remove的话,那么线程不注销,这些被set进去的数据也不会被注销。

反过来说,写代码中除非你清晰的认识到这个对象应该在哪里set,哪里remove,如果是模糊的,很可能你的代码中不会走remove的位置去,或导致一些逻辑问题,另外,如果不remove的话,就要等线程注销,我们在很多应用服务器中,线程是被复用的,因为在内核分配线程还是有开销的,因此在这些应用中线程很难会被注销掉,那么向ThreadLocal写入的数据自然很不容易被注销掉,这些可能在我们使用某些开源框架的时候无意中被隐藏用到,都有可能会导致问题,最后发现OOM得时候数据竟然来自ThreadLocalMap中,还不知道这些数据是从哪里设置进去的,所以你应当注意这个坑,可能不止一个人掉进这个坑里去过。

上一篇:Linux CentOS 7 防火墙/端口设置


下一篇:IIS7下,显示PHP错误(不显示500错误,而显示详细错误)