Java并发-ThreadLocal

1. 概述

ThreadLocal叫做线程变量,意思是 ThreadLocal 中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量

ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

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

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

特点:

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

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在单线程内被多个方法使用,换句话说,就是变量在多线程间隔离,而在各自的单线程内共享(类似于全局变量的概念) 的场景。

下图可以增强理解:

一句话理解 ThreadLocal,向 ThreadLocal 里面存东西就是向它里面的 ThreadLocalMap 存东西的,然后 ThreadLocal 把这个 ThreadLocalMap 挂到当前的线程底下,这样 ThreadLocalMap 就只属于这个线程了。


2. ThreadLocal与Synchronized的区别

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

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

  1. Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
  2. Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。
  3. ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而 Synchronized 却正好相反,它用于在多个线程间通信时能够获得数据共享

3. ThreadLocal的使用

  • public T get():获取该 ThreadLocal 变量对应 value 值。
  • public void set(T value):为该 ThreadLoca l变量设置 value 值。
  • public void remove():从 ThreadLocalMap 删除该 ThreadLocal 变量。
  • ThreadLocalMap getMap(Thread t):获取线程 Thread 所在的 ThreadLocalMap。
  • void createMap(Thread t, T firstValue):创建 ThreadLocalMap。
  • protected T initialValue():自定义类类型,需要重写该方法。

3.1 定义一个ThreadLocal变量

public class ThreadLocaDemo {
    public static ThreadLocal<String> localVar1 = new ThreadLocal<>();

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar1.get());
        //清除本地内存中的本地变量
        localVar1.remove();
    }
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            public void run() {
                localVar1.set("local_A");
                print("A");
                //打印本地变量
                System.out.println("after remove : " + localVar1.get());
            }
        },"A").start();

        Thread.sleep(1000);

        new Thread(new Runnable() {
            public void run() {
                localVar1.set("local_B");
                print("B");
                //打印本地变量
                System.out.println("after remove : " + localVar1.get());
            }
        },"B").start();
    }
}

输出:
A :local_A
after remove : null
B :local_B
after remove : null

从这个示例中我们可以看到,两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。

注:每个线程内 ThreadLocal 变量只能对应一个值,因为 ThreadLocal 变量做为 key 存到单线程的 ThreadLocalMap 中,根据 Map 唯一特性,所以单线程内 ThreadLocal 变量对应的值是唯一。

线程A执行localVar1.set("local_A_2");

new Thread(new Runnable() {
    public void run() {
        localVar1.set("local_A");
        localVar1.set("local_A_2");
        print("A");
        //打印本地变量
        System.out.println("after remove : " + localVar1.get());
    }
},"A").start();

输出:

A :local_A_2
after remove : null
B :local_B
after remove : null

线程A的localVar1变量对应的值变成了 “local_A_2”。

3.2 定义一个ThreadLocal变量,自定义类类型

方式一:需要重写 initialValue() 方法

public static ThreadLocal<User> localVar1 = new ThreadLocal<User>(){
    @Override
    protected User initialValue() {
        return new User();
    }
};

方式二:lamda方法

public static ThreadLocal<User> localVar1 = ThreadLocal.withInitial(() -> new User());

方式三:声明默认类型Object的ThreadLocal

public static ThreadLocal localVar1 = new ThreadLocal();

//使用时
User user = new User();
user.setName("lili");
localVar1.set(user);

3.3 定义多个ThreadLocal变量,并且声明不同类型。

增加 public static ThreadLocal<Map> localVar2 = new ThreadLocal<Map>(); ,声明类型为 Map

public class ThreadLocaDemo {
    public static ThreadLocal<String> localVar1 = new ThreadLocal<>();
    public static ThreadLocal<Map> localVar2 = new ThreadLocal<Map>();

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar1.get());
        //清除本地内存中的本地变量
        localVar1.remove();
    }
    static void print2(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar2.get());
        //清除本地内存中的本地变量
        localVar2.remove();
    }
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            public void run() {
                localVar1.set("local_A");
                print("A");
                //打印本地变量
                System.out.println("after remove : " + localVar1.get());

                Map<String, String> map = new HashMap<>();
                map.put("local_A_1","1001");
                map.put("local_A_2","1002");
                localVar2.set(map);
                print2("A");
                //打印本地变量
                System.out.println("after remove : " + localVar2.get());
            }
        },"A").start();

        Thread.sleep(1000);

        new Thread(new Runnable() {
            public void run() {
                localVar1.set("local_B");
                print("B");
                //打印本地变量
                System.out.println("after remove : " + localVar1.get());

                Map<String, String> map = new HashMap<>();
                map.put("local_B_1","2001");
                map.put("local_B_2","2002");
                localVar2.set(map);
                print2("B");
                //打印本地变量
                System.out.println("after remove : " + localVar2.get());
            }
        },"B").start();
    }
}

输出:
A :local_A
after remove : null
A :{local_A_1=1001, local_A_2=1002}
after remove : null
B :local_B
after remove : null
B :{local_B_1=2001, local_B_2=2002}
after remove : null

两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。


4. ThreadLocal的原理

public class ThreadLocal<T>
  • public T get():获取该 ThreadLocal 变量对应 value 值。
  • public void set(T value):为该 ThreadLoca l变量设置 value 值。
  • public void remove():从 ThreadLocalMap 删除该 ThreadLocal 变量。
  • ThreadLocalMap getMap(Thread t):获取线程 Thread 所在的 ThreadLocalMap。
  • void createMap(Thread t, T firstValue):创建 ThreadLocalMap。

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 属性不为空,用ThreadLocal作为key,更新value值;如果 map 为空,则初始化 ThreadLocalMap,并将 value 值初始化。

那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。

  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。使用弱引用,防止内存泄漏

弱引用的特点:如果一个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。详见《Java四大引用

//这个是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中删除。防止内存泄漏

4.4 ThreadLocal与Thread,ThreadLocalMap之间的关系

这个图中我们可以非常直观的看出,ThreadLocalMap 其实是 Thread 线程的一个属性值。ThreadLocalMap 可以拥有多个 ThreadLocal 维护的自己线程独享的共享变量(这个共享变量只是针对自己线程里面共享)。


5. ThreadLocal应用场景

基于ThreadLocal特点,适应场景

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

5.1 存储用户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;
    }

5.2 数据库连接,处理数据库事务

5.3 保存线程不安全的工具类SimpleDateFormat

通常用于保存线程不安全的工具类,典型的需要使用的类就是SimpleDateFormat

public class ThreadLocalDemo {
    // 定义线程池
    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        // 定义时间格式
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

        for (int j = 0; j < 60; j++) {
            int i = j;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    String time = simpleDateFormat.format(new Date(i * 1000L));
                    System.out.println(time);
                }
            });
        }
        threadPool.shutdown();
    }
}

结果可见,多线程共享同一个simpleDateFormat对象,会线程不安全

使用ThreadLocal:

public class ThreadLocalDemo {
    // 定义线程池
    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        // 定义时间格式
        //SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        ThreadLocal<SimpleDateFormat> simpleDateFormat = new ThreadLocal<SimpleDateFormat>(){
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            }
        };
        //lamda方式
        //ThreadLocal<SimpleDateFormat> simpleDateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

        for (int j = 0; j < 60; j++) {
            int i = j;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    //String time = simpleDateFormat.format(new Date(i * 1000L));
                    String time = simpleDateFormat.get().format(new Date(i * 1000L));
                    System.out.println(time);
                }
            });
        }
        threadPool.shutdown();
    }
}

在这里插入图片描述
线程安全。

ThreadLocal 给每个线程维护一个自己的 simpleDateFormat 对象,这个对象在线程之间是独立的,互相没有关系的。这也就避免了线程安全问题。与此同时,simpleDateFormat对象还不会创造过多,线程池一共只有 10 个线程,所以需要10个对象即可。

5.4 数据跨层传递

每个线程内需要保存类似于全局变量的信息,例如在拦截器中获取的用户信息,该信息需要被多个服务共享。

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

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

5.5 Spring使用ThreadLocal解决线程安全问题

Spring中,绝大部分 Bean 都可以声明为singleton作用域。就是因为 Spring 对一些Bean(如RequestContextHolderTransactionSynchronizationManagerLocaleContextHolder等)中非线程安全的“状态性对象” 采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的 Bean 就能够以 singleton 的方式在多线程中正常工作了。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9-2所示。

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

线程不安全示例

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

由于①处的cconnc是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。

线程安全示例

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();
	}

使用本地线程变量,保证了不同的线程使用线程相关的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到singleton共享了。


6. 面试题

1、为什么不将 ThreadLocalMap 的 key 设置为强引用?

如果 key 设计成强引用且没有手动 remove(),那么 key 会和 value 一样伴随线程的整个生命周期,是无法完全避免内存泄漏的。

2、ThreadLocalMap的 key 设置为弱引用,是不是避免了内存泄漏?

结论:无论ThreadLocalMap中的key使用什么引用方式,都无法完全避免泄漏。

假设在业务代码中使用完ThreadLocal,ThreadLocal ref被回收了。由于ThreadLocalMap只持有ThreadLocal的弱引用, 没有任何强引用指向ThreadLocal实例,因为弱引用特性,ThreadLocal 就可以顺利被gc回收。

但是!!此时Entry中的key = null,如果没有手动删除Entry以及CurrentThread依然运行的前提下,始终存在有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry,而且因为key=null,这块value永远不会被访问到了,value就不会被回收了,导致value内存泄漏

也就是说: ThreadLocalMap中的key使用了弱引用, 如果不手动remove(),也有可能内存泄漏。

3、为什么 key 要用弱引用?

事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的。

这就意味着如果没有手动删除Entry以及CurrentThread依然运行的前提下,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏。

4、如何正确的使用 ThreadLocal?

  1. 将ThreadLocal变量定义成private static的,这样的话 ThreadLocal 的生命周期就更长,由于一直存在 ThreadLocal 的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove()它,防止内存泄露。
  2. 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

5、ThreadLocal的 value为什么不是弱引用?

因为不清楚这个 Value 除了 map 的引用还是否还存在其他引用,如果不存在其他引用,当 GC 的时候就会直接将这个 Value 干掉了,而此时我们的 ThreadLocal 还处于使用期间,就会造成 Value 为null 的错误,所以将其设置为强引用。

参考文章:
史上最全ThreadLocal 详解(一)
史上最全ThreadLocal 详解(二)
ThreadLocal的应用场景

当前线程中获取所有的ThreadLocal

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值