话说在《操作系统原理》这门课里面,我们学到了很多概念:进程、线程、锁、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 = newThreadMapKey();

static ThreadMapKey logFd = newThreadMapKey();

static ThreadMapKey mysqlConnection = newThreadMapKey();

线程初始化的时候调用:

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类中的Key ,采用了弱引用的方式(而不是强引用),这样的好处,就是如果有对象不再使用的时候,就会被系统回收。

总结一下:在了解了实现原理以后,如果你有跟线程相关的数据而又可以不加锁的,就尽管使用ThreadLocal吧,真的很好用。他可以让你的程序有更高的效率,更好的代码整洁度。(而且还可以ZhuangBility!)

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

ThreadLocal 线程的私有内存的更多相关文章

  1. java ThreadLocal线程设置私有变量底层源码分析

    前面也听说了ThreadLocal来实现高并发,以前都是用锁来实现,看了挺多资料的,发现其实还是区别挺大的(感觉严格来说ThreadLocal并不算高并发的解决方案),现在总结一下吧. 高并发中会出现 ...

  2. 线程的私有领地 ThreadLocal

    从名字上看,『ThreadLocal』可能会给你一种本地线程的概念印象,可能会让你联想到它是一个特殊的线程. 但实际上,『ThreadLocal』却营造了一种「线程本地变量」的概念,也就是说,同一个变 ...

  3. 并发王者课-铂金10:能工巧匠-ThreadLocal如何为线程打造私有数据空间

    欢迎来到<并发王者课>,本文是该系列文章中的第23篇,铂金中的第10篇. 说起ThreadLocal,相信你对它的名字一定不陌生.在并发编程中,它有着较高的出场率,并且也是面试中的高频面试 ...

  4. java线程内存模型,线程、工作内存、主内存

    转自:http://rainyear.iteye.com/blog/1734311 java线程内存模型 线程.工作内存.主内存三者之间的交互关系图: key edeas 所有线程共享主内存 每个线程 ...

  5. ThreadLocal是否会导致内存泄露

    什么是内存泄露? 维基百科的定义:[内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存],我的理解就是程序失去了对某段内存的控制,那么这段内存就算是泄露了. ThreadLocal为什么会导致 ...

  6. 【java】ThreadLocal线程变量的实现原理和使用场景

    一.ThreadLocal线程变量的实现原理 1.ThreadLocal核心方法有这个几个 get().set(value).remove() 2.实现原理 ThreadLocal在每个线程都会创建一 ...

  7. 你的ThreadLocal线程安全么

    想必很多小伙伴们对ThreadLocal并不陌生,ThreadLocal叫做线程本地变量,也就是ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量.那么,我们 ...

  8. 【Java并发】线程安全和内存模型

    一.概述 1.1 什么是线程安全? 1.2 案例 1.3 线程安全解决办法: 二.synchronized 2.1 概述 2.2 同步代码块 2.3 同步方法 2.4 静态同步函数 2.5 总结 三. ...

  9. 一个神奇的bug:OOM?优雅终止线程?系统内存占用较高?

    摘要:该项目是DAYU平台的数据开发(DLF),数据开发中一个重要的功能就是ETL(数据清洗).ETL由源端到目的端,中间的业务逻辑一般由用户自己编写的SQL模板实现,velocity是其中涉及的一种 ...

随机推荐

  1. vue入门学习示例

    鄙人一直是用angular框架的,所以顺便比较了一下. <!DOCTYPE html> <html lang="en"> <head> < ...

  2. 【Linux资源管理】一款优秀的linux监控工具——nmon

    (一)nmon工具概述 nmon是以一个用来做linux服务器监控的工具,通过nmon,可以实现对以下参数的监控: --CPU使用率 --内存.交换空间使用率 --网络使用情况 --磁盘I/O,读写速 ...

  3. mybatis——学习笔记

    配置文件 <properties resource="dbconfig.properties"></properties> 1. properties 引入 ...

  4. #leetcode刷题之路37-解数独

    编写一个程序,通过已填充的空格来解决数独问题.一个数独的解法需遵循如下规则:数字 1-9 在每一行只能出现一次.数字 1-9 在每一列只能出现一次.数字 1-9 在每一个以粗实线分隔的 3x3 宫内只 ...

  5. Tomcat性能监控

    Tomcat性能监控工具很多,这里介绍两种1.JMeter 2.probe,使用这两种工具都需要在tomcat的安装目录/conf/tomcat-users.xml添加 <tomcat-user ...

  6. VirtualBox复制的虚拟机无法获取IP解决办法

    自从建立了这个账号后写了一篇,好几年没来了,今天来看看,顺便分享一下. 昨天晚上想玩玩zookeeper集群,在vb里复制了一台主机,可怎么也无法获取IP,经研究,终于还是解决了. 1.复制主机时勾选 ...

  7. 07JavaScript数据类型

    JavaScript 数据类型 值类型(基本类型):字符串(String).数字(Number).布尔(Boolean).对空(Null).未定义(Undefined).Symbol. 引用数据类型: ...

  8. 基于JQ的简版选项卡记录

    <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...

  9. 解决h5底部输入框在ios被软键盘顶飞 软键盘消失还下不来

    好吧,其实不是顶飞,准确点说应该是h5页面fiexed定位在底部的输入框在ios软键盘弹起的时候软键盘跟输入框有时会有一段悬空的距离,无法紧贴.在安卓机子上则没有这样的情况. 解决方法是通过h5的sc ...

  10. jsp内置的对象【jsp可用数据容器】

    jsp的内置对象用法:可以存放数据进去,本身页面可以调用,发生页面请求时,请求目标可以调用 理解:jsp就是一个大容器,有请求,响应等内置对象,会话需要从请求容器中提取 请求中内置session,发出 ...