Java ThreadLocal是什么

引子:SimpleDateFormat类

为什么SimpleDateFormat 是线程不安全的?

  • 这主要是因为,它内部使用了一个全局的 Calendar 变量,来存储 date 信息

解决方式:

  1. 在sdf.parse()前后加锁,这也是我们一般的处理思路。
  2. 手动改造代码,给每个线程都new一个SimpleDateFormat
  3. 使用JDK的ThreadLocal类(和2的思想相似)

2代码如下

public class ThreadSafeSDFUsingMap {
    private Map<Long, SimpleDateFormat> sdfMap = new ConcurrentHashMap();
	//key 是线程 ID,value 是 SimpleDateFormat
    public String formatIt(Date date) {
        Thread currentThread = Thread.currentThread();
        long threadId = currentThread.getId();

        SimpleDateFormat sdf = sdfMap.get(threadId);
        if (null == sdf) {
            sdf = new SimpleDateFormat("yyyyMMdd HHmm");
            sdfMap.put(threadId, sdf);
        }

        return sdf.format(date);
    }
}

3代码如下

static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();

public static class ParseDate implements Runnable {
    int i = 0;

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

    public void run() {
        try {
            if (tl.get() == null) {
                tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            }
            Date t = tl.get().parse("2015-03-29 19:29:" + i % 60);
            System.out.println(i + ":" + t);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

从这里也可以看到,为每一个线程人手分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。这点也需要大家注意。

ThreadLocal是什么

  • 用于创建线程的本地变量。可以使用get()\set()方法去获取他们的默认值或者在线程内部改变他们的值
  • 简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

ThreadLocal 的另一个用途

  • ThreadLocal 还有另一个用途,那就是保存线程上下文信息。这一点在很多框架乃至JDK类加载中都有用到。
  • 比如Spring的事务管理,方法A里头调用了方法B,方法B如果失败了,需要执行connection.rollback()来回滚事务。那么方法B怎么知道connection是哪个?最简单的就是方法A在调用方法B时,把connection对象传进去
  • 显然,这样很挫,需要修改方法的定义
  • 不过你现在知道ThreadLocal了,只需把connection塞入threadLocal,methodB和methodA在一个线程中执行,那么自然,methodB可以获取到和methodA相同的connection。
  • Spring 采用 Threadlocal 的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,具体可以参考Spring的TransactionSynchronizationManager类

总结ThreadLocal的两大用途

  1. 实现线程安全;
  2. 保存线程上下文信息;

当然ThreadLocal肯定还有更多的用途,只要我们弄懂了它的原理,就知道如何灵活使用。

ThreadLocal 的源代码

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

在set时,首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设入ThreadLocalMap中

get()方法先取得当前线程的ThreadLocalMap对象。然后,通过将自己作为key取得内部的实际数据

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)
            return (T)e.value;
    }
    return setInitialValue();
}

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

ThreadLocalMap

  • ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部的 Entry 也独立实现。
  • 在 ThreadLocalMap 中,也是用 Entry 来保存 K-V 结构数据的。但是 Entry 中 key 只能是 ThreadLocal 对象 ,这点被 Entry 的构造方法已经限定死了。
  • 和 HashMap 的最大的不同在于,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式:根据初始 key 的 hashcode 值确定元素在数组中的位置,如果发现这个位置上已经有其他 key 值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
  • 显然 ThreadLocalMap 采用线性探测的方式解决 Hash 冲突的效率很低,如果有大量不同的 ThreadLocal 对象放入 map 中时发生冲突,或者发生二次冲突,则效率很低。
  • 所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到 map 中的 Key 都是相同的 ThreadLocal,如果一个线程要保存多个变量,就需要创建多个 ThreadLocal,多个 ThreadLocal 放入 Map 中时会极大的增加 Hash 冲突的可能。

ThreadLocalMap 的问题

  • 由于 ThreadLocalMap 的 key 是引用,而 Value 是强引用。
  • 这就导致了一个问题,ThreadLocal 在没有外部对象强引用时,发生 GC 时弱引用 Key 会被回收,而 Value 不会回收,如果创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露

如何避免泄漏

  • 既然 Value 是强引用,那么我们要做的事,就是在调用 ThreadLocal 的 get()、set() 方法时完成后再调用 remove 方法,将 Entry 节点和 Map 的引用关系移除,这样整个 Entry 对象在 GC Roots 分析后就变成不可达了,下次 GC 的时候就可以被回收。
  • 如果使用 ThreadLocal 的 set 方法之后,没有显示的调用 remove 方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove 方法

ThreadLocal的key为什么设置成弱引用?value为什么不是弱引用?

假如key对ThreadLocal对象的弱引用,改为强引用。

即使ThreadLocal变量生命周期完了,设置成null了,但由于Entry中的key对ThreadLocal还是强引用。

就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。

假设该线程一直存在,那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。

但如果是弱引用,当ThreadLocal变量=null之后(强引用没了),只剩下key的弱引用,gc就会自动回收ThreadLocal对象

所以这里要理解的是:如果强引用和弱引用同时引用一个对象,那么这个对象不会被GC回收


Entry的value一般只被Entry引用,有可能没被业务系统中的其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。

参考:threadlocal value为什么不是弱引用?

Thread、ThreadLocal、ThreadLocalMap 的关系

在这里插入图片描述

ThreadLocal带来的性能提升

多线程下产生随机数的性能对比(多线程共用同一个Random类、使用ThreadLocal实现每个线程各自持有一个Random类)

注意Random类是线程安全的

package com.space.java.juc.threadlocal;

import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class RandomDemo {
  public static final int GEN_COUNT = 10000000;//生成随机数的数量
  public static final int THREAD_COUNT = 4;//线程数量
  static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);
  public static Random rnd = new Random(123);

  public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>() {
    // 当ThreadLocal get为null时,会调用此方法初始化
    @Override
    protected Random initialValue() {
      return new Random(123);
    }
  };

  public static class RndTask implements Callable<Long> {
    private int mode = 0;

    public RndTask(int mode) {
      this.mode = mode;
    }

    public Random getRandom() {
      if (mode == 0) {
        return rnd;
      } else if (mode == 1) {
        return tRnd.get();
      } else {
        return null;
      }
    }

    @Override
    public Long call() {
      long b = System.currentTimeMillis();
      for (long i = 0; i < GEN_COUNT; i++) {
        getRandom().nextInt();
      }
      long e = System.currentTimeMillis();
      System.out.println(Thread.currentThread().getName() + " spend " + (e - b) + "ms");
      return e - b;
    }
  }

  public static void main(String[] args) throws InterruptedException, ExecutionException {
    Future<Long>[] futs = new Future[THREAD_COUNT];
    for (int i = 0; i < THREAD_COUNT; i++) {
      futs[i] = exe.submit(new RndTask(0));
    }
    long totaltime = 0;
    for (int i = 0; i < THREAD_COUNT; i++) {
      totaltime += futs[i].get();
    }
    System.out.println("多线程访问同一个Random实例:" + totaltime + "ms");
    // ThreadLocal的情况
    for (int i = 0; i < THREAD_COUNT; i++) {
      futs[i] = exe.submit(new RndTask(1));
    }
    totaltime = 0;
    for (int i = 0; i < THREAD_COUNT; i++) {
      totaltime += futs[i].get();
    }
    System.out.println("使用ThreadLocal包装Random实例:" + totaltime + "ms");
    exe.shutdown();
  }
}

ThreadLocalRandom类

Random是线程安全的,但由于内部使用cas机制,导致它不是高并发的

而ThreadLocalRandom是一个性能强悍的高并发随机数生成器,ThreadLocalRandom继承自Random

和ThreadLocal的原理类似,Thread类内部有一些变量专门用于让随机数生成器只访问本地线程数据,从而避免竞争

基本使用方法:

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        new Player().start();
    }
}

private static class Player extends Thread {
    @Override
    public void run() {
        System.out.println(getName() + ": " + ThreadLocalRandom.current().nextInt(100));
    }
}

用于生成随机订单编号

private static final SimpleDateFormat dateFormatOne=new SimpleDateFormat("yyyyMMddHHmmssSS");

private static final ThreadLocalRandom random=ThreadLocalRandom.current();

/**
 * 生成订单编号-方式一
 * @return
 */
public static String generateOrderCode(){
    //时间戳+N为随机数流水号
    return dateFormatOne.format(DateTime.now().toDate()) + generateNumber(4);
}

//N为随机数流水号
public static String generateNumber(final int num){
    StringBuffer sb=new StringBuffer();
    for (int i=1;i<=num;i++){
        sb.append(random.nextInt(9));
    }
    return sb.toString();
}

参考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值