Java多线程之ThreadLocal线程局部变量详解

概述

ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。

线程局部变量(ThreadLocal)的功能非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的。在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。map的键就是每个线程对象,值就是每个线程所拥有的值

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

API说明

  • ThreadLocal():创建一个线程本地变量
  • T get():返回此线程局部变量的当前线程的初始值。若第一次调用则会调用内部initialValue 方法,initialValue初始值为null
  • void remove():移除此线程的局部变量值,若再次get则会得到null
  • void set(T value): 将指定值设置为当前线程的局部变量值
  • protected T initialValue():返回此线程局部变量的当前线程的初始值,初始值为null,一般在使用的时候,通过匿名内部类重写该方法,给一个默认的特定值

实例

每个线程访问同一个变量,独立的输出1~10

public class ThreadLocalDemo {
    private  static  ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        public Integer initialValue(){
            return 0;
        }
    };

    class SequenceNumberRandom {
        public int getNextNum() {
            int m = threadLocal.get() + 1;
            threadLocal.set(m);
            return threadLocal.get();
        }
    }

    class Client extends Thread {
        private SequenceNumberRandom r;

        public Client(SequenceNumberRandom r) {
            this.r = r;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " : "
                        + r.getNextNum());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        SequenceNumberRandom r = demo.new SequenceNumberRandom();

        Client c1 = demo.new Client(r);
        Client c2 = demo.new Client(r);
        Client c3 = demo.new Client(r);
        Client c4 = demo.new Client(r);

        c1.start();
        c2.start();
        c3.start();
        c4.start();
    }
}

使用场景

  • 比如用户登录后,每个用户都有一个LoginContext,但是我们不知道哪个业务组件会需要这个LoginContext。

    最简单也是最常用的方式是业务组件自己在接口中明确指出自己的方法需要一个LoginContext,但是如果我们的业务组件发生了变量,比如安全校验机制变了,那么我们是需要修改接口的,这会很麻烦,甚至修改扩展到整个系统。

    那么上面的一种解决方案就是,我们为用户的操作加入一个拦截器,然后在拦截器中把用户的LoginContext放入一个单例对象的ThreadLocal变量中,业务组件可以从这个单例对象中读取信息。这样就不需要在接口中明确指明需不需要LoginContext了

  • 在SSO登录系统中,用户的每步操作都可能需要校验登录时系统产生的Token,这个时候我们把整改Token扩展到系统中是不合适的,所以把token存在一个单例对象中心,token用ThreadLocal存储

  • 在获取数据库连接connection、管理用户session场景,都可以使用ThreadLocal

总结

  • ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每个线程中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。

  • ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。

  • ThreadLocal和Synchonized都用于解决多线程并发访问,但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

  • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。Synchronized靠的Java锁机制实现,ThreadLocal靠的是给每个线程建立一个Map实现的,线程销毁则这个Map销毁


版权声明:本文为supermao1013原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。