• (转)Java多线程学习之ThreadLocal源码分析


    (转) http://www.cnblogs.com/moongeek/p/7857794.html

    ThreadLocal,即线程本地变量。它可以将变量绑定到特定的线程上的入口,使每个线程都拥有改变量的一个拷贝,各线程相同变量间互不影响,是实现共享资源的轻量级同步。

      下面是个ThreadLocal使用的实例,两个任务共享同一个变量,并且两个任务都把该变量设置为了线程私有变量,这样,虽然两个任务都”持有“同一变量,但各自持有该变量的拷贝。因此,当一个线程修改该变量时,不会影响另一线程该变量的值。

    复制代码
     1 public class LocalTest1 implements Runnable {
     2     // 一般会把 ThreadLocal 设置为static 。它只是个为线程设置局部变量的入口,多个线程只需要一个入口
     3     private static ThreadLocal<Student> localStudent = new ThreadLocal() {
     4         // 一般会重写初始化方法,一会分析源码时候会解释为什么
     5         @Override
     6         public Student initialValue() {
     7             return new Student();
     8         }
     9     };
    10 
    11     private Student student = null;
    12 
    13     @Override
    14     public void run() {
    15         String threadName = Thread.currentThread().getName();
    16 
    17         System.out.println("【" + threadName + "】:is running !");
    18 
    19         Random ramdom = new Random();
    20         //随机生成一个变量
    21         int age = ramdom.nextInt(100);
    22 
    23         System.out.println("【" + threadName + "】:set age to :" + age);
    24         // 获得线程局部变量,改变属性值
    25         Student stu = getStudent();
    26         stu.setAge(age);
    27 
    28         System.out.println("【" + threadName + "】:第一次读到的age值为 :" + stu.getAge());
    29 
    30         try {
    31             TimeUnit.SECONDS.sleep(2);
    32         } catch (InterruptedException e) {
    33             e.printStackTrace();
    34         }
    35 
    36         System.out.println("【" + threadName + "】:第二次读到的age值为 :" + stu.getAge());
    37     }
    38 
    39     public Student getStudent() {
    40         student = localStudent.get();
    41 
    42         // 如果不重写初始化方法,则需要判断是否为空,然后手动为ThreadLocal赋值
    43 //        if(student == null){
    44 //            student = new Student();
    45 //            localStudent.set(student);
    46 //        }
    47       
    48         return student;
    49     }
    50 
    51  public static void main(String[] args) {
    52         LocalTest1 ll = new LocalTest1();
    53         Thread t1 = new Thread(ll, "线程1");
    54         Thread t2 = new Thread(ll, "线程2");
    55 
    56         t1.start();
    57         t2.start();
    58     }
    59 }
    60 
    61 public class Student{
    62     private int age;
    63 
    64     public Student(){
    65 
    66     }
    67     public Student(int age){
    68         this.age = age;
    69     }
    70 
    71     public int getAge() {
    72         return age;
    73     }
    74 
    75     public void setAge(int age) {
    76         this.age = age;
    77     }
    78 }
    复制代码

    运行结果:

    复制代码
    【线程1】:is running !
    【线程2】:is running !
    【线程2】:set age to :45
    【线程1】:set age to :25
    【线程1】:第一次读到的age值为 :25
    【线程2】:第一次读到的age值为 :45
    【线程1】:第二次读到的age值为 :25
    【线程2】:第二次读到的age值为 :45
    复制代码

    1、ThreadLocal 源码分析

      ThreadLocal 源码有很多方法,但是暴露出来的公共接口只有三个:

    public ThreadLocal{
      public T get() {}
      public void set(T value) {}
      public void remove() {}
    }

       set(T value) 是设置局部变量的方法,源码如下:

    复制代码
     1 public void set(T value) {
     2       // 获得当前线程
     3     Thread t = Thread.currentThread();
     4       // 获得当前线程的 ThreadLocalMap 引用,详细见下
     5     ThreadLocalMap map = getMap(t);
     6       // 如果不为空,则更新局部变量的值
     7     if (map != null)
     8       map.set(this, value);
     9       //如果不是第一次使用,先进行初始化
    10     else
    11       createMap(t, value);
    12  }
    复制代码

    getMap(t) 源码如下,每一个Thread变量都自带了一个ThreadLocalMap类型的成员变量,用于保存该线程的成员变量。

    1 ThreadLocalMap getMap(Thread t) {
    2   //返回该线程Thread的成员变量threadLocals
    3    return t.threadLocals;
    4 }

      但是,Thread 默认把threadLocals设置为了null,因此第一次使用局部变量时候需要先初始化。

    ThreadLocal.ThreadLocalMap threadLocals = null;

      ThreadLocalMap 是定义在ThreadLocal 类里的内部类,它的作用是存储线程的局部变量。ThreadLocalMap 以ThreadLocal的引用作为键,以局部变量作为值,存储在ThreadLocalMap.Entry (一种存储键值的数据结构)里。关于ThreadLocalMap 的源码,后文会详细介绍,这里只要知道大概原理即可。

      由此我们可以总结ThreadLocal 的设计思想如下:

    1. ThreadLocal  只是个访问局部变量的入口。
    2. 局部变量的值存在线程Thread 类本地,默认为null,只有通过ThreadLocal 访问时才会进行初始化。
    3. [ThreadLocalMap 的设计思路在后文介绍ThreadLocalMap  源码时候会分析]

    get() 是获得线程本地变量,源码如下:

    复制代码
     1 public T get() {
     2       //获得当前线程    
     3     Thread t = Thread.currentThread();
     4       //得到当前线程的一个threadLocals 变量
     5     ThreadLocalMap map = getMap(t);
     6     if (map != null) {
     7       // 如果不为空,以当前ThreadLocal为主键获得对应的Entry
     8       ThreadLocalMap.Entry e = map.getEntry(this);
     9       if (e != null) {
    10         @SuppressWarnings("unchecked")
    11         T result = (T)e.value;
    12         return result;
    13       }
    14     }
    15       //如果值为空,则进行初始化
    16     return setInitialValue();
    17 }
    复制代码

    再来看看初始化函数setInitialValue() 所进行的操作:

    复制代码
     1 private T setInitialValue() {
     2       //获得初始默认值
     3     T value = initialValue();
     4       //得到当前线程
     5     Thread t = Thread.currentThread();
     6       // 获得该线程的ThreadLocalMap引用
     7     ThreadLocalMap map = getMap(t);
     8       //不为空则覆盖
     9     if (map != null)
    10         map.set(this, value);
    11     else
    12           //若是为空,则进行初始化,键为本ThreadLocal变量,值为默认值
    13         createMap(t, value);
    14 }
    15 
    16 // 默认初始化返回null值,这也是为什么需要重写该方法的原因。如果没有重写,第一次get()操作获得的线程本地变量为null,需要进行判断并手动调用set()进行初始化
    17 protected T initialValue() {
    18     return null;
    19 }
    复制代码

    2、ThreadLocalMap 源码分析

      Thread类中包含一个ThreadLocalMap 类型的成员变量threadLocals,这是直接存储线程局部变量的数据结构。ThreadLocal 只是一个入口,通过ThreadLocal操作threadLocals,进行局部变量的查改操作。这也是为什么ThreadLocal 暴露的公有接口才三个的原因吧。同时,由于ThreadLocalMap 中的键是ThreadLocal类,也说明了,如果想为一个线程设置多个本地局部变量,需要设置多个 ThreadLocal。下面来分析下ThreadLocalMap 的源码。

      ThreadLocalMap  里有几个核心的属性,和HashMap相似:

    复制代码
    // table 默认大小,大小为2的次方,用于hash定位
    private static final int INITIAL_CAPACITY = 16;
    // 存放键值对的数组
    private Entry[] table;
    // 扩容的临界值,当table元素大到这个值,会进行扩容
    private int threshold;
    复制代码

      在调用ThreadLocal 中的set(T) 方法时,调用了ThreadLocalMap 的set(ThreadLocal, T) 方法,

    复制代码
     1  private void set(ThreadLocal<?> key, Object value) {
     2      Entry[] tab = table;
     3      int len = tab.length;
     4         // Hash 寻址,与table数组长度减1(二进制全是1)相与,所以数组长度必须为2的次方,减小hash重复的可能性
     5      int i = key.threadLocalHashCode & (len-1);
     6 
     7        //从hash值计算出的下标开始遍历
     8      for (Entry e = tab[i];
     9           e != null;
    10           e = tab[i = nextIndex(i, len)]) {
    11        //获得该Entry的键
    12        ThreadLocal<?> k = e.get();
    13         //如果键和传过来的相同,覆盖原值,也说明,一个ThreadLocal变量只能为一个线程保存一个局部变量
    14        if (k == key) {
    15          e.value = value;
    16          return;
    17        }
    18        // 键为空,则替换该节点
    19        if (k == null) {
    20          replaceStaleEntry(key, value, i);
    21          return;
    22        }
    23      }
    24     
    25      tab[i] = new Entry(key, value);
    26      int sz = ++size;
    27        //是否需要扩容
    28      if (!cleanSomeSlots(i, sz) && sz >= threshold)
    29        rehash();
    30  }
    复制代码

      为什么说数组长度为2的次方有利于hash计算不重复呢?我们来看下,显然,和一个二进制全是1的数相于,能最大限度的保证原数的所有位数,因而重复几率会变小。

      可以看出ThreadLocalMap 采用线性探测再散列解决Hash冲突的问题。即,如果一次Hash计算出来的数组下标被占用,即hash值重复了,则在该下标的基础上加1测试下一个下标,直到找到空值。比如说,Hash计算出来下标i为6,table[6] 已经有值了,那么就尝试table[7]是否被占用,依次类推,直到找到空值。以上,就是保存线程本地变量的方法。

      再来分析下ThreadLocal 中的get() 方法,其中调用了ThreadLocalMap 的map.getEntry(this) 方法,并把本ThreadLocal作为参数传入,返回一个ThreadLocalMap.Entry对象(以后简称Entry),源码如下:

    复制代码
     1 private Entry getEntry(ThreadLocal<?> key) {
     2       //Hash计算数组下标
     3     int i = key.threadLocalHashCode & (table.length - 1);
     4       //得到该下标的节点
     5     Entry e = table[i];
     6       //如果该节点存在,并且键和传过来的ThreadLocal对象相同,则返回该节点(说明该节点没有进行Hash冲突处理)
     7     if (e != null && e.get() == key)
     8       return e;
     9       //如果该节点不直接满足需求,可能进行了Hash冲突处理,则另外处理
    10     else
    11       return getEntryAfterMiss(key, i, e);
    12 }
    复制代码

      再来分析下getEntryAfterMiss(ThreadLocal, int , Entry) 的源码:

    复制代码
     1 //  if (e == null || e.get() != key)
     2 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
     3     Entry[] tab = table;
     4     int len = tab.length;
     5     //从洗标为i开始遍历,直到遇到下一空节点或或是满足需求的节点
     6     while (e != null) {
     7         ThreadLocal<?> k = e.get();
     8         if (k == key)
     9             return e;
    10         if (k == null)
    11               //节点不为空,键为空,则清理该节点
    12             expungeStaleEntry(i);
    13         else
    14               // i后移
    15             i = nextIndex(i, len);
    16         e = tab[i];
    17     }
    18       //否则返回空值
    19     return null;
    20 }
    复制代码

      以上就是ThreadLocalMap 几个比较关键的源码分析。

      

      综上所述可知,ThreadLocal 只是访问Thread本地变量的一个入口,正真存储本地变量的其实是在Thread本地,同时ThreadLocal也作为一个键去Hash找到变量所在的位置。也许你会想,为什么不把ThreadLocalMap设置为< Thread,Variable>类型,把Thread作为主键,而要增加一个中间模块ThreadLocal?我的想法是,一来,这样确实可以满足需求,但是这样无法进行hash查找,如果一个Thread的本地变量过多,通过线性查找会花费大量时间,使用ThreadLocal作为中间键,可以进行Hash查找;二来,其实本地变量的添加、查找和删除需要进行大量的操作,设计者的思路是把这些操作封装在一个ThreadLocal类里,而只暴露了三个常用的接口,如果把ThreadLocal去掉,这些操作可能要写在Thread类里,违背了设计类的“单一性”原则;三来,我们这样相当于为每个本地变量取了个“名字”(即,一个ThreadLocal对应一个本地变量),使用方便。

  • 相关阅读:
    Django之Models(一)
    数据库学习之事务
    pymysql的使用
    pymysql:Mysql拒绝从远程访问的解决办法
    Django之模板基础
    Django之视图函数总结
    POJ1942
    poj2115[扩展欧几里德]
    POJ1850&&POJ1496
    [Catalan数]1086 栈、3112 二叉树计数、3134 Circle
  • 原文地址:https://www.cnblogs.com/latteyan/p/7879643.html
Copyright © 2020-2023  润新知