zoukankan      html  css  js  c++  java
  • java之ThreadLocal详解

    一、ThreadLocal简介

    ThreadLocal是线程的局部变量,是每一个线程所单独持有的,其他线程不能对其进行访问,通常是类中的private static字段。

    我们知道有时候一个对象的变量会被多个线程所访问,这时就会有线程安全问题,当然我们可以使用synchorinized 关键字来为此变量加锁,进行同步处理,从而限制只能有一个线程来使用此变量,但是加锁会大大影响程序执行效率,此外我们还可以使用ThreadLocal来解决对某一个变量的访问冲突问题。

    当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。

    但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

    二、ThreadLocal源码分析

    (1)ThreadLocal方法

    ThreadLocal 的几个方法: ThreadLocal 可以存储任何类型的变量对象, get返回的是一个Object对象,但是我们可以通过泛型来制定存储对象的类型。

    public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
    public void set(T value) { } //set()用来设置当前线程中变量的副本
    public void remove() { } //remove()用来移除当前线程中变量的副本
    protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的
    

    (2)ThreadLocal实现原理

    1.内部结构

    Thread 在内部是通过ThreadLocalMap来维护ThreadLocal变量表, 在Thread类中有一个threadLocals 变量,是ThreadLocalMap类型的,它就是为每一个线程来存储自身的ThreadLocal变量的。

    ThreadLocal.ThreadLocalMap threadLocals = null;
    

     ThreadLocalMap 是定义在ThreadLocal 类里的内部类,它的作用是存储线程的局部变量。ThreadLocalMap 以ThreadLocal的引用作为键,以局部变量作为值,存储在ThreadLocalMap.Entry (一种存储键值的数据结构)里。这是因为在每一个线程里面,可能存在着多个ThreadLocal变量

    static class ThreadLocalMap {
     static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
    

    2.调用过程

    初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
    然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找

    注意:

    在进行get之前,必须先set,否则会报空指针异常;

    如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
      

    ThreadLocal的set(T value)方法

    public void set(T value) {
      // 获得当前线程
    Thread t = Thread.currentThread();
      // 获得当前线程的 ThreadLocalMap 引用,详细见下
    ThreadLocalMap map = getMap(t);
      // 如果不为空,则更新局部变量的值
    if (map != null)
      map.set(this, value);
      //如果不是第一次使用,先进行初始化
    else
      createMap(t, value);
    }
    

    内部类ThreadLocalMap的set(ThreadLocal<?> key,Object value)

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

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

    TheadLocal的get()方法

    public T get() {
      //获得当前线程    
    Thread t = Thread.currentThread();
      //得到当前线程的一个threadLocals 变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      // 如果不为空,以当前ThreadLocal为主键获得对应的Entry
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
      }
    }
      //如果值为空,则进行初始化
    return setInitialValue();
    }
    

    ThreadLocal的setInitialValue()方法

    private T setInitialValue() {
      //获得初始默认值
    T value = initialValue();
      //得到当前线程
    Thread t = Thread.currentThread();
      // 获得该线程的ThreadLocalMap引用
    ThreadLocalMap map = getMap(t);
      //不为空则覆盖
    if (map != null)
        map.set(this, value);
    else
          //若是为空,则进行初始化,键为本ThreadLocal变量,值为默认值
        createMap(t, value);
    }
    
    // 默认初始化返回null值,这也是 下面demo 为什么需要重写该方法的原因。如果没有重写,第一次get()操作获得的线程本地变量为null,需要进行判断并手动调用set()进行初始化
    protected T initialValue() {
        return null;
    }
    

    三、Demo

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

    package thread.ThreadLocalTest;
    
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    
    /**
    * Created by StoneGeek on 2018/8/1.
    * 博客地址:http://www.cnblogs.com/sxkgeek
    */
    public class ThreadLocalDemo2 implements Runnable {
        // 一般会把 ThreadLocal 设置为static 。它只是个为线程设置局部变量的入口,多个线程只需要一个入口
        private static ThreadLocal<Student> localStudent = new ThreadLocal() {
        // 一般会重写初始化方法,一会分析源码时候会解释为什么
            @Override
            public Student initialValue() {
                return new Student();
        }
    };
    
    private Student student = null;
    
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
    
        System.out.println("【" + threadName + "】:is running !");
    
        Random ramdom = new Random();
        //随机生成一个变量
        int age = ramdom.nextInt(100);
    
        System.out.println("【" + threadName + "】:set age to :" + age);
        // 获得线程局部变量,改变属性值
        Student stu = getStudent();
        stu.setAge(age);
    
        System.out.println("【" + threadName + "】:第一次读到的age值为 :" + stu.getAge());
    
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        System.out.println("【" + threadName + "】:第二次读到的age值为 :" + stu.getAge());
    }
    
    public Student getStudent() {
        student = localStudent.get();
    
    //         如果不重写初始化方法,则需要判断是否为空,然后手动为ThreadLocal赋值,否则的话会报空指针异常
    //        if(student == null){
    //            student = new Student();
    //            localStudent.set(student);
    //        }
    
        return student;
    }
    
    public static void main(String[] args) {
        ThreadLocalDemo2 ll = new ThreadLocalDemo2();
        Thread t1 = new Thread(ll, "线程1");
        Thread t2 = new Thread(ll, "线程2");
    
        t1.start();
        t2.start();
    }
    }
    
    console打印:
    【线程2】:is running !
    【线程1】:is running !
    【线程1】:set age to :67
    【线程2】:set age to :4
    【线程1】:第一次读到的age值为 :67
    【线程2】:第一次读到的age值为 :4
    【线程1】:第二次读到的age值为 :67
    【线程2】:第二次读到的age值为 :4
    

    四、应用场景

    最常见的ThreadLocal使用场景为
    用来解决 数据库连接、Session管理等。

    数据库连接

    Class A implements Runnable{
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
        public Connection initialValue() {
                return DriverManager.getConnection(DB_URL);
        }
    };
    
    public static Connection getConnection() {
           return connectionHolder.get();
    }
    }
    

    Session管理

    private static final ThreadLocal threadSession = new ThreadLocal();
    
    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }
  • 相关阅读:
    Angular与PHP之间的不同的请求方式(post/get)导致的传参问题
    项目当中会使用到的---获取主机名
    JavaScript eval() 函数
    PHP 数组排序
    PHP Switch 语句
    PHP strlen()函数和strpos()函数
    Array.prototype.map()和Array.prototypefilter()
    25.参考链接
    24.ArrayBuffer
    23.读懂 ECMAScript 规格
  • 原文地址:https://www.cnblogs.com/sxkgeek/p/9406463.html
Copyright © 2011-2022 走看看