一、ThreadLocal 简介

ThreadLocal 叫做_线程变量,意思是 ThreadLocal 中填充的变量_属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就_不存在多线程间共享的问题_。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被 private static 修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

下图可以增强理解:

                                                                 图 1-1  ThreadLocal 在使用过程中状态

二、ThreadLocal 与 Synchronized 的区别

ThreadLocal 其实是与线程绑定的一个变量。ThreadLocal 和 Synchonized 都用于解决多线程并发访问。

但是 ThreadLocal 与 synchronized 有本质的区别:

1、Synchronized 用于线程间的数据共享,而 ThreadLocal 则用于线程间的数据隔离。

2、Synchronized 是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而 ThreadLocal 为每一个线程都提供了变量的副本

,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

而 Synchronized 却正好相反,它用于在多个线程间通信时能够获得数据共享。

一句话理解 ThreadLocal,threadlocl 是作为当前线程中属性 ThreadLocalMap 集合中的某一个 Entry 的 key 值 Entry(threadlocl,value),虽然不同的线程之间 threadlocal 这个 key 值是一样,但是不同的线程所拥有的 ThreadLocalMap 是独一无二的,也就是不同的线程间同一个 ThreadLocal(key)对应存储的值 (value) 不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个 value 变量地址是一样的。

三、ThreadLocal 的简单使用

直接上代码:

public class ThreadLocaDemo {
 
    private static ThreadLocal<String> localVar = new ThreadLocal<String>();
 
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) throws InterruptedException {
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_A");
                print("A");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
               
            }
        },"A").start();
 
        Thread.sleep(1000);
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_B");
                print("B");
                System.out.println("after remove : " + localVar.get());
              
            }
        },"B").start();
    }
}
 
A :local_A
after remove : null
B :local_B
after remove : null
 

从这个示例中我们可以看到,两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。这个的理解也可以结合图 1-1,相信会有一个更深刻的理解。

四、ThreadLocal 的原理

要看原理那么就得从源码看起。

  4.1 ThreadLocal 的 set() 方法:

 public void set(T value) {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化thradLocalMap 并赋值
            createMap(t, value);
    }

         从上面的代码可以看出,ThreadLocal  set 赋值的时候首先会获取当前线程 thread, 并获取 thread 线程中的 ThreadLocalMap 属性。如果 map 属性不为空,则直接更新 value 值,如果 map 为空,则实例化 threadLocalMap, 并将 value 值初始化。

那么 ThreadLocalMap 又是什么呢,还有 createMap 又是怎么做的,我们继续往下看。大家最后自己再 idea 上跟下源码,会有更深的认识。

  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<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 
        
    }

    可看出 ThreadLocalMap 是 ThreadLocal 的内部静态类,而它的构成主要是用 Entry 来保存数据 ,而且还是继承的弱引用。在 Entry 内部使用 ThreadLocal 作为 key,使用我们设置的 value 作为 value。详细内容要大家自己去跟。

//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 
 
    //ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

         4.2 ThreadLocal 的 get 方法

    public T get() {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map数据不为空,
        if (map != null) {
            //3.1、获取threalLocalMap中存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
        return setInitialValue();
    }
 
 
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

4.3 ThreadLocal 的 remove 方法

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

 remove 方法,直接将 ThrealLocal 对应的值从当前相差 Thread 中的 ThreadLocalMap 中删除。为什么要删除,这涉及到内存泄露的问题。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocal 其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将 ThreadLocal 内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与 JVM 的生命周期一致。举个例字,如果 ThreadLocal 中直接或间接包装了集合类或复杂对象,每次在同一个 ThreadLocal 中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

 4.4、ThreadLocal 与 Thread,ThreadLocalMap 之间的关系  

图 4-1 Thread、THreadLocal、ThreadLocalMap 之间啊的数据关系图

从这个图中我们可以非常直观的看出,ThreadLocalMap 其实是 Thread 线程的一个属性值,而 ThreadLocal 是维护 ThreadLocalMap

这个属性指的一个工具类。Thread 线程可以拥有多个 ThreadLocal 维护的自己线程独享的共享变量(这个共享变量只是针对自己线程里面共享)

五、ThreadLocal 常见使用场景

如上文所述,ThreadLocal 适用于如下两种场景

  • 1、每个线程需要有自己单独的实例
  • 2、实例需要在多个方法中共享,但不希望被多线程共享

对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

场景

1)存储用户 Session

一个简单的用 ThreadLocal 来存储 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;
    }

场景二、数据库连接,处理数据库事务

场景三、数据跨层传递(controller,service, dao)

      每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户 ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用 service-1()、service-2()、service-3()、service-4(),这 4 个方法可能是分布在不同的类中的。这个例子和存储 session 有些像。

package com.kong.threadlocal;
 
 
public class ThreadLocalDemo05 {
    public static void main(String[] args) {
        User user = new User("jack");
        new Service1().service1(user);
    }
 
}
 
class Service1 {
    public void service1(User user){
        //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
        UserContextHolder.holder.set(user);
        new Service2().service2();
    }
}
 
class Service2 {
    public void service2(){
        User user = UserContextHolder.holder.get();
        System.out.println("service2拿到的用户:"+user.name);
        new Service3().service3();
    }
}
 
class Service3 {
    public void service3(){
        User user = UserContextHolder.holder.get();
        System.out.println("service3拿到的用户:"+user.name);
        //在整个流程执行完毕后,一定要执行remove
        UserContextHolder.holder.remove();
    }
}
 
class UserContextHolder {
    //创建ThreadLocal保存User对象
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
 
class User {
    String name;
    public User(String name){
        this.name = name;
    }
}
 
执行的结果:
 
service2拿到的用户:jack
service3拿到的用户:jack

场景四、Spring 使用 ThreadLocal 解决线程安全问题 

我们知道在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在 Spring 中,绝大部分 Bean 都可以声明为 singleton 作用域。就是因为 Spring 对一些 Bean(如 RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder 等)中非线程安全的 “状态性对象” 采用 ThreadLocal 进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的 Bean 就能够以 singleton 的方式在多线程中正常工作了。 
一般的 Web 应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图 9-2 所示。 
 

这样用户就可以根据需要,将一些非线程安全的变量以 ThreadLocal 存放,在同一次请求响应的调用线程中,所有对象所访问的同一 ThreadLocal 变量都是当前线程所绑定的。

下面的实例能够体现 Spring 对有状态 Bean 的改造思路:

代码清单 9-5  TopicDao:非线程安全

 
public class TopicDao {
   //①一个非线程安全的变量
   private Connection conn; 
   public void addTopic(){
        //②引用非线程安全变量
	   Statement stat = conn.createStatement();
	   …
   }

由于①处的 conn 是成员变量,因为 addTopic()方法是非线程安全的,必须在使用时创建一个新 TopicDao 实例(非 singleton)。下面使用 ThreadLocal 对 conn 这个非线程安全的 “状态” 进行改造: 
代码清单 9-6  TopicDao:线程安全 

 
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
 
  //①使用ThreadLocal保存Connection变量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
         
	    //②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
        //并将其保存到线程本地变量中。
if (connThreadLocal.get() == null) {
			Connection conn = ConnectionManager.getConnection();
			connThreadLocal.set(conn);
              return conn;
		}else{
              //③直接返回线程本地变量
			return connThreadLocal.get();
		}
	}
	public void addTopic() {
 
		//④从ThreadLocal中获取线程对应的
         Statement stat = getConnection().createStatement();
	}

不同的线程在使用 TopicDao 时,先判断 connThreadLocal.get() 是否为 null,如果为 null,则说明当前线程还没有对应的 Connection 对象,这时创建一个 Connection 对象并添加到本地线程变量中;如果不为 null,则说明当前的线程已经拥有了 Connection 对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的 Connection,而不会使用其他线程的 Connection。因此,这个 TopicDao 就可以做到 singleton 共享了。 
当然,这个例子本身很粗糙,将 Connection 的 ThreadLocal 直接放在 Dao 只能做到本 Dao 的多个方法共享 Connection 时不发生线程安全问题,但无法和其他 Dao 共用同一个 Connection,要做到同一事务多 Dao 共享同一个 Connection,必须在一个共同的外部类使用 ThreadLocal 保存 Connection。但这个实例基本上说明了 Spring 对有状态类线程安全化的解决思路。在本章后面的内容中,我们将详细说明 Spring 如何通过 ThreadLocal 解决事务管理的问题。

后续在补充

下一篇:史上最全 ThreadLocal 详解(二)

大家帮我看看这篇文章:一文搞懂 AQS,AQS 从入门到源码阅读

参考:

ThreadLocal 的应用场景 - sw_kong - 博客园

百度安全验证

ThreadLocal 使用场景分析 - 简书

ThreadLocal 原理分析与使用场景 - 阿凡卢 - 博客园

ThreadLocal 原理分析与使用场景 - 阿凡卢 - 博客园

ThreadLocal 使用场景分析 - 简书

(转)spring 里面对 ThreadLocal 的使用_迷茫之欲的博客 - CSDN 博客_spring threadlocal