《实战Java高并发程序设计》笔记——ThreadLocal


一、简单的使用

1.1 是什么

ThreadLocal是一个线程的局部变量,只有当前线程可以访问,因为只有当前线程可以访问,所以是线程安全的。

1.2 示例

1、举例
多线程环境中使用SimpleDateFormat 解析日期
2、代码

public class SimpleDateFormatTest {
    // SimpleDateFormat 实例
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    // 解析日期任务类
    public static class ParseDate implements Runnable {
        private int i = 0;

        public ParseDate(int i) {this.i = i;}

        @Override
        public void run() {
            try {
                Date date = sdf.parse("2020-10-06 19:21:" + i % 60);
                System.out.println(i + " : " + date);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 创建线程池,解析日期
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            pool.execute(new ParseDate(i));
        }
    }
}

3、执行结果

Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: multiple points

可以看到执行后抛出异常,所以SimpleDateFormat.parse()方法不是线程安全的

1.3 解决方案

方案一:在parse方法前后加锁
方案二:使用ThreadLocal

/**
 * @author QianYi
 * @version 1.0
 * @Description 使用ThreadLocal解析日期
 */
public class x02ParseDate {
    // SimpleDateFormat 实例
    private static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();

    // 日期解析任务类
    public static class ParseDate implements Runnable {
        private int i = 0;

        public ParseDate(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            try {
                // 如果ThreadLocal中没有SimpleDateFormat实例,那就初始化一个
                if (tl.get() == null) {
                    tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                }
                Date date = tl.get().parse("2020-10-06 19:21:" + i % 60);
                System.out.println(i + " : " + date);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 创建线程池,解析日期
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            pool.execute(new ParseDate(i));

        }
    }
}

为每一个线程分配不同的对象,需要在应用层进行保证;ThreadLocal只是起到了容器的作用


二、实现原理

2.1 set() 方法

我们来看看 set() 方法的内部

public void set(T value) {
    // 1、获取当前线程对象
    Thread t = Thread.currentThread();
    // 2、获取当前线程对象的 ThreadlLocalMap
    ThreadLocalMap map = getMap(t);
    // 3、将值存入 ThreadlLcaolMap 中
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

1、可以简单地把ThreadlLocalMap看作是一个Map(但请注意,它不是HashMap,而是存储 key-value 结构的 Entry)。
2、ThreadlLocalMap 是定义在Thread类中地成员,如下代码所示:

ThreadLocal.ThreadLocalMap threadLocals = null;	// Thread 类中

我们设置的数据写入了 threadLocals 中,其中 key 为 ThreadLocal 当前对象,value 就是我们设置的值。threadLocals 保存了当前自己线程所有的“局部变量”,也就是一个ThreadLocal变量的集合。

2.2 get() 方法

public T get() {
    // 1、获取当前线程 
    Thread t = Thread.currentThread();
    // 2、获取当前线程的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    // 3、将自己作为 key 取得内部的数据
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            return (T) e.value;
        }
    }
    return setInitialValue();
}

ThreadLocal 中的一些变量是维护在Threa类内部的,所以:只要线程不退出,对象的引用就一直存在,无法被GC回收
当线程退出时,Thread类会进行一些清理操作(包括清理ThreadLocalMap)。通过将引用置为null,使得JVM将其当作垃圾并进行回收

private void exit() {
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    // 加速资源清理
    target = null;
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
}

2.3 ThreadLocal 导致内存泄漏

1、问题
如上面所说,线程不退出,对象引用就一直存在,这在线程池中可能会导致内存泄漏。
例如固定大小的线程池(FixThreadPool),其线程总是存在。如果将一些大对象设置进了 ThreadLocal 中,使用了几次后就不再使用,同时也没有清理它,这会导致大对象无法被回收,最终导致内存泄露。
2、如何解决

  • 使用 ThreadLocal.remove() 方法移除这个变量
  • 使用类似于 obj = null 的代码

3、为何使用类似于 obj = null 的代码可以防止内存泄漏

  • ThreadLocalMap使用了弱引用(弱引用就是:在垃圾回收时,JVM 发现弱引用,就直接将其回收)。
  • ThreadLocalMap 内部由一系列 Entry 构成,每个Entry都继承了弱引用 WeakRefefence,如下代码所示:
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

其中 k 是 ThreadLocal 实例,作为弱引用使用。super(k)就是调用了 WeakReference 的构造函数;
因此虽然使用 ThreadLocal 作为 Entry 的 k,但实际上它并不持有 ThreadLocal 的引用。
当 ThreadLocal 的外部强引用被回收时,Entry 中的 k 就会变成 null。
当进行垃圾回收时,就会将这些垃圾数据回收。
在这里插入图片描述


三、对性能的帮助

3.1 说明

如果共享对象对于竞争的处理容易引起性能损失,我们还是考虑使用ThreadLocal为每个线程分配单独的对象

3.2 代码示例

我们使用一个随机数的例子来说明性能。

  • 定义全局变量
// 要生成的随机数数量
private static final int GEN_COUNT = 10000000;
// 线程数量
private static final int THREAD_NUM = 4;
// 线程池
private static ExecutorService pool = Executors.newFixedThreadPool(THREAD_NUM);
// 多线程共享的 Random 实例
private static Random rnd = new Random(123);
// ThreadLocal 封装的 Random 实例
private static ThreadLocal<Random> tRnd = new ThreadLocal<Random>() {
    @Override
    protected Random initialValue() {
        return new Random(123);
    }
};
  • 定义任务类
public static class RndTask implements Callable<Long> {
    // 0:使用多线程共享的 Random 的实例
    // 1:使用 ThreadLocal 封装的 Random 实例
    private int mode;

    public RndTask(int mode) {
        if (mode != 0 || mode != 1) throw new RuntimeException();
        this.mode = mode;
    }

    // 根据 mode 的值获取 random 实例
    public Random getRandom() {
        return mode == 0 ? rnd : tRnd.get();
    }

    // 打印并返回生成随机数的时间
    @Override
    public Long call() throws Exception {
        long start = System.currentTimeMillis();
        for (int i = 0; i < GEN_COUNT; i++) {
            getRandom().nextInt();
        }
        long end = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + " spend " + (end - start) + "ms");
        return end - start;
    }
}
  • 定义主函数
public static void main(String[] args) throws ExecutionException, InterruptedException {
    // **** 测试一:使用多线程共享的 Random 的实例 ****
    Future<Long>[] future01 = new Future[GEN_COUNT];
    // 生成随机数
    for (int i = 0; i < THREAD_NUM; i++) {
        future01[i] = pool.submit(new RndTask(0));
    }
    // 统计时间
    long time01 = 0;
    for (int i = 0; i < THREAD_NUM; i++) {
        time01 += future01[i].get();
    }
    System.out.println("使用多线程共享的 Random 的实例: " + time01 + " ms");

    // **** 测试二:使用 ThreadLocal 封装的 Random 实例 ****
    Future<Long>[] future02 = new Future[GEN_COUNT];
    // 生成随机数
    for (int i = 0; i < THREAD_NUM; i++) {
        future02[i] = pool.submit(new RndTask(1));
    }
    // 统计时间
    long time02 = 0;
    for (int i = 0; i < THREAD_NUM; i++) {
        time02 += future02[i].get();
    }
    System.out.println("使用 ThreadLocal 封装的 Random 实例: " + time02 + " ms");
}
  • 执行结果
pool-1-thread-3 spend 2860ms
pool-1-thread-4 spend 2896ms
pool-1-thread-2 spend 2896ms
pool-1-thread-1 spend 2900ms
使用多线程共享的 Random 的实例: 11552 ms
pool-1-thread-1 spend 119ms
pool-1-thread-4 spend 121ms
pool-1-thread-3 spend 122ms
pool-1-thread-2 spend 123ms
使用 ThreadLocal 封装的 Random 实例: 485 ms

从上面可以看出,使用 ThreadLocal 可以大大提高性能。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值