谈谈Java中的ThreadLocal

PrimaryKey 8年前
   <h2>什么是ThreadLocal</h2>    <p>ThreadLocal一般称为 <strong> 线程本地变量, </strong> 它是一种特殊的线程绑定机制,将变量与线程 <strong>绑定</strong> 在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内。</p>    <p>跳出误区</p>    <p>需要重点强调的的是,不要拿ThreadLocal和synchronized做类比,因为这种比较压根就是无意义的!sysnchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的正确访问。而ThreadLocal从本质上讲,无非是提供了一个 <strong>“线程级”</strong> 的 <strong>变量作用域</strong> ,它是一种 <strong>线程封闭</strong> (每个线程独享变量)技术,更直白点讲,ThreadLocal可以理解为将对象的作用范围限制在一个 <strong>线程上下文</strong> 中,使得变量的作用域为“ <strong>线程级</strong> ”。</p>    <p>没有ThreadLocal的时候,一个线程在其声明周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;而ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。</p>    <p>来看个简单的例子</p>    <p>假设我们要为每个线程关联一个唯一的序号,在每个线程周期内,我们需要多次访问这个序号,这时我们就可以使用ThreadLocal了.(当然下面这个例子没有完全体现出跨层级跨方法的调用,理解就可以了)</p>    <pre>  <code class="language-java">package concurrent;    import java.util.concurrent.atomic.AtomicInteger;    /**   * Created by chengxiao on 2016/12/12.   */  public class ThreadLocalDemo {      public static void main(String []args){          for(int i=0;i<5;i++){              final Thread t = new Thread(){                  @Override                  public void run(){                      System.out.println("当前线程:"+Thread.currentThread().getName()+",已分配ID:"+ThreadId.get());                  }              };              t.start();          }      }      static   class ThreadId{          //一个递增的序列,使用AtomicInger原子变量保证线程安全          private static final AtomicInteger nextId = new AtomicInteger(0);          //线程本地变量,为每个线程关联一个唯一的序号          private static final ThreadLocal<Integer> threadId =                  new ThreadLocal<Integer>() {                      @Override                      protected Integer initialValue() {                          return nextId.getAndIncrement();//相当于nextId++,由于nextId++这种操作是个复合操作而非原子操作,会有线程安全问题(可能在初始化时就获取到相同的ID,所以使用原子变量                      }                  };           //返回当前线程的唯一的序列,如果第一次get,会先调用initialValue,后面看源码就了解了          public static int get() {              return threadId.get();          }      }  }  </code></pre>    <p>执行结果,可以看到每个线程都分配到了一个唯一的ID,同时在此线程范围内的"任何地点",我们都可以通过ThreadId.get()这种方式直接获取。</p>    <pre>  <code class="language-java">当前线程:Thread-4,已分配ID:1  当前线程:Thread-0,已分配ID:0  当前线程:Thread-2,已分配ID:3  当前线程:Thread-1,已分配ID:4  当前线程:Thread-3,已分配ID:2   </code></pre>    <h2>看看源码</h2>    <p> set操作,为线程绑定变量</p>    <pre>  <code class="language-java"> public void set(T value) {      Thread t = Thread.currentThread();//1.首先获取当前线程对象          ThreadLocalMap map = getMap(t);//2.获取该线程对象的ThreadLocalMap          if (map != null)              map.set(this, value);//如果map不为空,执行set操作,以当前threadLocal对象为key,实际存储对象为value进行set操作          else              createMap(t, value);//如果map为空,则为该线程创建ThreadLocalMap      }  </code></pre>    <p>可以看到,ThreadLocal不过是个入口,真正的变量是绑定在线程上的。</p>    <pre>  <code class="language-java">ThreadLocalMap getMap(Thread t) {      return t.threadLocals;//线程对象持有ThreadLocalMap的引用  }  </code></pre>    <p>下面给是Thread类中的定义,每个线程对象都拥有一个ThreadLocalMap对象</p>    <pre>  <code class="language-java">    ThreadLocal.ThreadLocalMap threadLocals = null;  </code></pre>    <p>现在,我们能看出ThreadLocal的设计思想了:</p>    <p>1.ThreadLocal仅仅是个变量访问的入口;</p>    <p>2.每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有对象的引用;</p>    <p>3.ThreadLocalMap以当前的threadlocal对象为key,以真正的存储对象为value。get时通过threadlocal实例就可以找到绑定在当前线程上的对象。</p>    <p>乍看上去,这种设计确实有些绕。我们完全可以在设计成Map<Thread,T>这种形式,一个线程对应一个存储对象。ThreadLocal这样设计的目的主要有两个:</p>    <p>一是可以保证当前线程结束时相关对象能尽快被回收;二是ThreadLocalMap中的元素会大大减少,我们都知道map过大更容易造成哈希冲突而导致性能变差。</p>    <p>我们再来看看get方法</p>    <pre>  <code class="language-java"> public T get() {       Thread t = Thread.currentThread();//1.首先获取当前线程           ThreadLocalMap map = getMap(t);//2.获取线程的map对象           if (map != null) {//3.如果map不为空,以threadlocal实例为key获取到对应Entry,然后从Entry中取出对象即可。               ThreadLocalMap.Entry e = map.getEntry(this);               if (e != null)                   return (T)e.value;           }           return setInitialValue();//如果map为空,也就是第一次没有调用set直接get(或者调用过set,又调用了remove)时,为其设定初始值       }  </code></pre>    <pre>  <code class="language-java"><strong>setInitialValue</strong></code></pre>    <pre>  <code class="language-java"> 1  private T setInitialValue() {   2         T value = initialValue();//获取初始值   3         Thread t = Thread.currentThread();   4         ThreadLocalMap map = getMap(t);   5         if (map != null)   6             map.set(this, value);   7         else   8             createMap(t, value);   9         return value;  10     }  </code></pre>    <p>initialValue方法,默认是null,访问权限是protected,即允许重写。</p>    <pre>  <code class="language-java">1 protected T initialValue() {  2         return null;  3     }  </code></pre>    <p>谈到这儿,我们应该已经对ThreadLocal的设计目的及设计思想有一定的了解了。</p>    <h2>线程独享变量?</h2>    <p>还有一个会引起疑惑的问题,我们说ThreadLocal为每一个线程维护一个独立的 <strong>变量副本</strong> ,那么是不是说各个线程之间真正的做到对于对象的 <strong>“完全自治”</strong> 而不对其他线程的对象产生影响呢?其实这已经不属于对于ThreadLocal的讨论,而是你出于何种目的去使用ThreadLocal。如果我们为一个线程关联的对象是“完全独享”的,也就是每个线程拥有一整套的新的 <strong>栈中的对象引用+堆中的对象,</strong> 那么这种情况下是真正的彻底的“ <strong>线程独享变量</strong> ”,相当于一种深度拷贝,每个线程自己玩自己的,对该对象做任何的操作也不会对别的线程有任何影响。</p>    <p>另一种更普遍的情况,所谓的独享变量副本,其实也就是每个线程都拥有一个独立的对象引用,而堆中的对象还是 <strong>线程间共享</strong> 的,这种情况下,自然还是会涉及到对共享资源的访问操作,依然会有线程不安全的风险。所以说,ThreadLocal无法解决线程安全问题。</p>    <p>所以,需不需要完全独享变量,进行完全隔离,就取决于你的应用场景了。可以想象,对象过大的时候,如果每个线程都有这么一份“深拷贝”,并发又比较大,对于服务器的压力自然是很大的。像web开发中的servlet,servlet是线程不安全的,一请求一线程,多个线程共享一个servlet对象;而早期的CGI设计中,N个请求就对应N个对象,并发量大了之后性能自然就很差。</p>    <p><img src="https://simg.open-open.com/show/fafe5a79bb0a117e39763bc055450885.png"></p>    <pre>  <code class="language-java">        ThreadLocal在spring的事务管理,包括Hibernate的session管理等都有出现,在web开发中,有时会用来管理用户会话 HttpSession,web交互中这种典型的一请求一线程的场景似乎比较适合使用ThreadLocal,但是需要特别注意的是,由于此时session与线程关联,而tomcat这些web服务器多会采用线程池机制,也就是说线程是可复用的,所以在每一次进入的时候都需要重新进行set,或者在结束时及时remove。</code></pre>    <p> </p>    <p>来自:http://www.cnblogs.com/chengxiao/p/6152824.html</p>    <p> </p>