Java多线程之ThreadLocal

ProjectDaedalus

共 10773字,需浏览 22分钟

 · 2021-09-06

本文介绍下Java多线程方面高频出现的ThreadLocal类

abstract.jpeg

基本实践

在传统的多线程开发场景中,为了避免多个线程同时操作一个共享变量而引起并发的问题。通常会通过加锁的形式进行处理。特别是在这个共享变量,并不是一个所谓的共享资源而只是用于线程内部各方法传递、使用的参数时,这种加锁的并发控制显然会降低系统的吞吐量。而ThreadLocal类则给我们提供一个新的思路——线程本地私有存储数据。简单来说就是,ThreadLocal为共享变量在每个线程内部提供了一个副本,用于进行线程内部自身的访问、存储等操作。避免了该共享变量在多线程环境下的操作冲突。该类典型方法如下所示

// 设置Value
public void set(T value);

// 获取数据
public T get();

// 移除数据
public void remove();

这里通过一个Demo来了解下该如何使用

package com.aaron.ThreadLocalTest;

import java.util.Random;

public class Demo1 {

    public static void main(String[] args) throws Exception{
        Runnable task = new MyTask();

        new Thread( task ).start();
        new Thread( task ).start();

    }

}

class MyTask implements Runnable {

    private Integer num = null;

    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        // 每个线程随机生产一个数
        Integer count = new Random().nextInt(100);
        System.out.println(Thread.currentThread().getName() + ", count: " + count );

        // 模拟业务耗时
        try{
            Thread.sleep(5000);
        }catch (Exception e) {
        }

        // 存储数据
        num = count;
        threadLocal.set(count);

        // 获取数据
        System.out.println( Thread.currentThread().getName() + ", num: " + num + ", threadLocal: " +threadLocal.get() );

        // 移除当前线程所存的数据
        threadLocal.remove();
    }
}

通过上述示例及其测试结果,可以看出对于普通的共享变量num而言,在多线程操作过程中会发生冲突。具体表现为Thread-0线程下该变量本来为53,却又被Thread-1线程修改为74;而对于threadLocal变量而言,从测试结果直观上我们就可看出并未在多线程环境下发生冲突,各线程的threadLocal变量数据被线程隔离了

figure 1.jpeg

实现原理

前面我们提到ThreadLocal是通过线程本地私有存储数据实现线程安全的。这里结合ThreadLocal、Thread类的源码做进一步阐述。以set方法为例,其首先通过currentThread获取当前线程的Thread实例。并通过getMap方法获取该线程的threadLocals属性,即一个ThreadLocal.ThreadLocalMap实例。而在ThreadLocalMap则通过Entry实现对ThreadLocal及其值的存储。进一步地,为了支持存储多个ThreadLocal变量及其值,ThreadLocalMap类中提供一个Entry数组类型的table字段。get方法同理,不再赘述。简而言之,ThreadLocal之所以可以实现对数据的线程隔离,是因为其将数据存储到Thread实例中

public class ThreadLocal<T{

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

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

        private Entry[] table;

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

}

...

public class Thread implements Runnable {

    ThreadLocal.ThreadLocalMap threadLocals = null;

}

应用场景

线程内资源的复用

众所周知,时间格式化类SimpleDateFormat在多线程环境下是非线程安全的。为此传统的解决方案,要么通过加锁的方式实现,此举会产生阻塞显著降低效率;要么则定义为局部变量,每次使用需自行new该实例,如果任务的数量较多,显然会严重浪费内存、CPU等资源。而ThreadLocal则可以很好的解决该问题。将SimpleDateFormat实例与线程实例进行绑定。一方面,各线程使用不同的SimpleDateFormat实例,避免了SimpleDateFormat线程不安全问题;另一方面,在基于线程池的方式利用线程的场景下,该线程所绑定的SimpleDateFormat实例在本次任务完成后,可以在该线程下一次的任务中继续复用。避免根据任务频繁地创建SimpleDateFormat实例。示例Demo如下所示

public class Demo2 {

    public static void main(String[] args) throws Exception{
        Runnable task = new Task();

        new Thread( task ).start();
        new Thread( task ).start();
    }

}

class Task implements Runnable {

    private SimpleDateFormat dateFormat1 = new SimpleDateFormat("HH:mm:ss");

   // 使用static进行修饰,保持对ThreadLocal实例的强引用。这样只要该线程不结束退出,该SimpleDateFormat即可通过dateFormat2重复访问、使用
    private static ThreadLocal<SimpleDateFormat> dateFormat2 = ThreadLocal.withInitial(
        () -> new SimpleDateFormat("HH:mm:ss")
    );

    @Override
    public void run() {
        Long ts = RandomUtil.randomLong(10L5000000L);
        Date date = new Date(ts);
        System.out.println(Thread.currentThread().getName() + ", date: " + date );

        String str1 = dateFormat1.format(date);
        String str2 = dateFormat2.get().format(date);

        System.out.println( Thread.currentThread().getName() + ", str1: " + str1 + ", str2: " + str2 );

    }

}

测试结果如下所示,两个线程str1输出的结果证明了SimpleDateFormat的非线程安全

figure 2.jpeg

传递上下文

鉴于ThreadLocal的线程隔离特性,可以很方便我们在一个线程内的多个方法进行参数传递。即所谓的Context上下文。典型地,包括用户身份信息、数据库连接信息等。以避免通过添加入参的形式进行传递

内存泄露

Entry的Key

前面我们提到在ThreadLocalMap内部是通过Entry实现对ThreadLocal及其值的存储。而Entry的key字段则是一个指向ThreadLocal实例的弱引用。这里对弱引用 WeakReference 作必要的补充说明:GC进行回收时,对于只具有弱引用的对象,不管当前内存空间是否充足,均会回收该对象。故当一个ThreadLocal实例没有外部强引用时,其必然可以被GC回收,显然利用static修饰的ThreadLocal变量除外。试想如果Entry的key字段是一个指向ThreadLocal实例的强引用,那么如果该线程永远不结束退出,则会导致ThreadLocal实例无法被回收

Entry的Value

需要注意的是,Entry存储value则是通过强引用进行关联的。结合前面通过对Entry的key进行分析可知,一旦ThreadLocal实例不存在外部的强引用而被GC回收后,则相应的Entry实例就会变为key为null而value依然存在强引用。除非该线程退出结束,否则该value对象将会一直被Entry实例强引用而无法进行回收。这也是大家通常所说的ThreadLocal存在内存泄露的根源所在。其实,在ThreadLocal的set、get方法内部实现中也会对Entry数组进行检查,对key为null的Entry实例进行清除。但显然这种清除的触发是有一定的条件。故推荐大家在使用完ThreadLocal后,通过remove方法显式地清除该value值。或者将ThreadLocal变量修饰为static属性,以保持对ThreadLocal实例的强引用。这样就能保证任何时候均可以通过ThreadLocal实例对value进行更新、清除等操作

浏览 47
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报