自打老王的公众号( simplemain )开了《有问有答》这个菜馆儿以后,生意超级好。老王也是很开心能给大家服务。这周有盆友就问了老王一个技术问题,关于线程私有数据的。老王之前也遇到过类似的问题,就想把这个事情总结总结,然后分享给大家,希望大家在工作中(或者以后的工作中)能用的上。
好了,老王以前口水话都很多,这周就少说点,直接开始正题吧。
话说在《操作系统原理》这门课里面,我们学到了很多概念:进程、线程、锁、 PV 操作、读写者问题等等,大家还记得么?(估计有些概念早已忘记了吧,哈哈哈 ~ ) 其中关于进程、线程和锁的东西是我们平时工作中用到最多的:服务器接收到用户请求,需要用一个进程或者一个线程去处理,然后操作内存、文件或者数据库的时候,可能需要对他们进行加锁操作。这一切都看起来顺理成章,正常的不能再正常,对吧。
不过作为一个有追求的程序员,我们有些时候会不满足于此,会想方设法的去追求卓越和 ZhuangBility ,这些也是老王所追求的 ^_^ 。于是,我们开始对线程、锁开始了漫漫的优化之路。其中有一种情况是非常值得优化的:假定我们现在有一个 web 服务,我们的程序一般都会为每一个到来的请求创建一个上下文环境(比如:请求的 URL 、浏览器型号、访问的 token 等等),然后这个上下文贯穿了这一次请求,任何处理操作都可以拿到这个上下文。那我们实现的时候,就可以这样来做:
我们设计一个 ContextManager ,用来管理所有的 context 。他可能是一个单例( singleton )模式,也可能提供静态( static )方法,反正不管如何,全局都可以访问,我们这里为了说明简单,就假定是用 static 方法。然后请求的处理线程在接收到任务的时候,就调用这个 ContextManager 的 getContext() 方法,提供给这个线程一个 context ,然后线程对这个 context 做初始化等等操作,供后续使用。
我们来考虑一下 ContextManager.getContext() 的实现:为了保证多个线程访问时候的安全,我们在绝大多数情况下,会对这个函数加锁(或者部分代码块加锁),对吧。这正如我们之前所说,是再正常不过的操作了。不过,如果访问量很大的话,这个加锁就会导致性能下降(特别是线程很多的时候,这个情况就越发的明显),很多线程会在锁上进行等待( linux 下可以用 strace , java 可以用 jstack 等工具来查看),其造成的结果就是处理速度变慢,并发能力下降。那有没有好的解决方案呢?答案是肯定的(不然老王怎么往下讲 ^_^ )
第一次改进:全局锁 -> 线程内部数据
大家想,对于 context 而言,只要有用户请求进来,他的这一段青春就已经献给了这次请求,换句话说,实际上就是把自己献给了这个线程,你中有我,我中有你。那既然这样,我们是不是就可以把 context 的归属权让给 thread ,而不是 ContextManager 呢?这样, context 不光是一段青春给了 thread ,更是把一生都献给了 thread~
伪代码 大体上就写成了这样:
class Thread
{
private Context context;
public Context getContext()
{
return context;
}
}
如果一个请求过来了,我们的工作线程直接就初始化自己的 context 环境,然后供后续逻辑处理使用,再也不用去求别人加个锁分配个 context 了。是不是一个很棒的方案呢?
只要少去了锁,效率确实就极大的提升,看起来我们就不用往下讲了,因为之前说的问题都解决了,是吗?
其实不完全,比方说,除了 context ,我还要放点其他的东西,如:日志文件的 fd 、数据库的连接…… 这怎么办呢?一种办法是,我们在 Thread 这个类里面,再继续填充各种东西,比如:
class Thread
{
private Context context;
private int logFd;
private MysqlConnection conn;
}
那如果再加东西呢?按照这种方式,不但写起来麻烦,而且扩展能力相当差。我们怎么改进呢?
第二次改进:线程内成员归一到 Map
其实也很简单,我们不是有一种神奇的数据结构叫做 map (有红黑树、 Hash 等实现版本)么?他就能帮我们来管理这些烂七八糟的东东啊。
于是乎,我们原来的那些代码,就可以这样的修改啦:
class Thread
{
private Map<String, Object> threadMap;
public Map<String, Object> getThreadMap()
{
return threadMap;
}
}
线程创建并初始化的时候,执行以下代码:
Threadthread = Thread.getCurrentThread();
Map<String,Object> map = thread.getThreadMap();
map.put( "context" , new Context());
map.put( "logFd" , ( new Log()).getFd());
map.put( "mysqlConnection" , ConnectionPool.getConnection());
这样,我们的代码就有非常好的一个扩展性了,想放啥放啥,对吧。而且请求来了以后,也不加锁,程序跑的飞快!如果你的程序要放些啥进去,也没啥问题。不过,就是唯一有点问题,你的程序要记住各种字符串组成的 key ,比如: "context" 、 "logFd" 等等。虽然也不是什么问题吧,不过也有些不是太完美。而且如果代码是多个人写的话,还有可能出现 key 的命名冲突,对吧(谁把谁覆盖了都不知道,然后各种 debug ,最后发现了问题,只能说一句:我擦!)。
那我们又该怎么样来解决这个问题呢?
其实也不难,既然字符串容易造成混乱,我们把字符串换掉,换成一个不重复的东西不就结了嘛?那什么东西不会重复呢?很简单,内存地址啊!于是,我们就可以把代码改成这样:
class ThreadMapKey
{}
class Thread
{
private Map<ThreadMapKey, Object> threadMap;
public Map<ThreadMapKey, Object> getThreadMap()
{
return threadMap;
}
}
全局建立几个对象:
static ThreadMapKey context = new ThreadMapKey();
static ThreadMapKey logFd = new ThreadMapKey();
static ThreadMapKey mysqlConnection = new ThreadMapKey();
线程初始化的时候调用:
Thread thread = Thread.getCurrentThread();
Map<ThreadMapKey, Object> map = thread.getThreadMap();
map.put(context, new Context());
map.put(logFd, ( new Log()).getFd());
map.put(mysqlConnection, ConnectionPool.getConnection());
我们定义一个叫做 ThreadMapKey 的类,这个类啥事儿不干,他就是一个摆设。当全局初始化的时候,我们新建几个他的实例,比如: context 、 logFd 、 mysqlConnection 等,然后把他们当做 ThreadMap 的 Key 。这样,不同的开发者再也不用担心自己起的名字会冲突了,因为只要对象不一样,他们的内存地址就是不一样的,我们用他做的 Key 就是不一样的。
好了,这样看起来似乎已经很完美了。不过呢,对于追求极致美的程序员而言,他们不甘心,觉得还有瑕疵:每次要从这个线程取线程局部数据的时候,代码写起来都麻烦的很。具体看如下:
Thread thread = Thread.getCurrentThread();
Map<ThreadMapKey,Object> map = thread.getThreadMap();
Object obj = map.get(context);
Context value = (Context)obj;
这样的代码看起来似乎太不优美了,要写这么多行代码…… 我们如何优化呢?
如果我们把上述代码包装起来,是不是就不用每次都写了呢?那怎么包装呢?我们的 ThreadMapKey 就是一个很好的东东,我们就让他提供一个函数,用来包装。说干就干,看看代码:
class ThreadMapKey <T>
{
public T getValue()
{
Thread thread = Thread.getCurrentThread();
Map<ThreadMapKey, Object>map = thread.getThreadMap();
Object obj = map.get( this );
T value = (T)obj;
return value;
}
public void setValue(T value)
{
Thread thread =Thread.getCurrentThread();
Map<ThreadMapKey, Object>map = thread.getThreadMap();
map.put( this , value);
}
}
class Thread
{
private Map<ThreadMapKey, Object> threadMap;
public Map<ThreadMapKey, Object> getThreadMap()
{
return threadMap;
}
}
static ThreadMapKey context = new ThreadMapKey();
static ThreadMapKey logFd = new ThreadMapKey();
static ThreadMapKey mysqlConnection = new ThreadMapKey();
// init
context.setValue( new Context());
logFd.setValue(( new Log()).getFd());
mysqlConnection.setValue(ConnectionPool.getConnection());
// get per query
Context value = context.getValue();
我们将 ThreadMapKey 这个类增加了两个方法: getValue 和 setValue ,他们分别从当前线程中取出 ThreadMap ,然后根据 this 关键字来获取和设置 value 。而且用到了泛型,这样取出来的值就可以直接赋值,而不用再自己写代码来做类型强转。这下再看代码,是不是简介了很多很多。
=== 进入最终的话题 ===
上面讲了这么多(都是老王 YY 的,没有经过任何组织的认证,如果讲的不对,大家就包含着哈,关键是大家看懂了就好 ^_^ ),其实这些都是 java 的一个叫做 ThreadLocal 类引出来的事儿。老王最初在学校里看 ThreadLocal 的代码就看的有点绕,一会儿是 ThreadLocal ,一会儿又是 Map ,一会儿又是 Thread 。然后代码跳来跳去,可能当时记住了,后来久了不看又忘了。用的时候也是容易产生错误的用法(百度里去搜索,大部分都是雷同的,完全没把这个事情讲清楚)。
后来去百度工作了,在实际的项目中,用到了线程数据这样的东东,才发现:哦,原来线程这玩意儿可以私藏数据,然后还不用加锁,真是好用!(在百度用的是 c/c++ ,原理是一样的)。用多了,再去看 ThreadLocal 的代码,就自己琢磨和猜测他的演化过程了。
好了,我们拿 ThreadLocal 的代码和我们上面的代码做一个对应吧:
我们把
ThreadMapKey ->ThreadLocal ,
ThreadMap -> ThreadLocalMap
做了这样的一个转换以后,再来看 Java 的代码:
public class ThreadLocal<T> {
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();
}
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 class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null ;
}
看看,是不是和我们的实现很像啊。我们的应用代码怎么写呢?
private static final ThreadLocal <Session> sessions = new ThreadLocal <>();
public static Session getThreadSession()
{
Session session = sessions .get();
if (session == null )
{
session = new Session();
sessions .set(session);
}
return session;
}
这样的使用是不是感觉很有 B 格呢?
好了,原理性的东西说完了,还有一点需要补充一下,就是在 ThreadLocalMap 的实现代码里写的很赞的一个点:
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<?>> {
/** Thevalue associated with this ThreadLocal. */
Object value ;
Entry(ThreadLocal<?> k, Object v){
super (k);
value = v;
}
}
}
ThreadLocalMap 的实现,没有用标准的 Map ,而是自己实现了一个 HashMap (有兴趣的同学可以去读读源代码,就是我们教科书上讲的经典 hash 实现)。这个 Map 里的 Entry 类,采用了弱引用的方式(而不是强引用),这样的好处,就是如果有对象不再使用的时候,就会被系统回收,而不是被这个 Map 继续持有引用,而造成系统不可回收,进而使得内存泄露(此处给 Java 代码的实现者鼓掌!)
总结一下:在了解了实现原理以后,如果你有跟线程相关的数据而又可以不加锁的,就尽管使用 ThreadLocal 吧,真的很好用。他可以让你的程序有更高的效率,更好的代码整洁度。(而且还可以 ZhuangBility !)
好了,今天扯淡就扯这么多吧,都看懂了嘛? 如果还没关注老王 ( simplemain ),就赶紧 吧。老王在每周日的下午,与各位不见不散 ~