文章目录
- 引子:SimpleDateFormat类
- ThreadLocal是什么
- ThreadLocal 的另一个用途
- **总结**ThreadLocal的两大用途
- ThreadLocal 的源代码
- ThreadLocalMap
- ThreadLocalMap 的问题
- ThreadLocal的key为什么设置成弱引用?value为什么不是弱引用?
- Thread、ThreadLocal、ThreadLocalMap 的关系
- ThreadLocal带来的性能提升
- ThreadLocalRandom类
- 参考
引子:SimpleDateFormat类
为什么SimpleDateFormat 是线程不安全的?
- 这主要是因为,它内部使用了一个全局的 Calendar 变量,来存储 date 信息
解决方式:
- 在sdf.parse()前后加锁,这也是我们一般的处理思路。
- 手动改造代码,给每个线程都new一个SimpleDateFormat
- 使用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的两大用途
- 实现线程安全;
- 保存线程上下文信息;
当然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();
}
参考
- 《实战Java高并发程序设计》4.3节
- 拼多多面试官没想到ThreadLocal我用得这么溜,人直接傻掉
- threadlocal value为什么不是弱引用?
- 高并发情况下你还在用Random生成随机数?