Java并发编程学习03—彻底搞懂ThreadLocal

一、什么是Threadlocal

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

ThreadLocal示例

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}

输出:

Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm

可以看出每一个线程在改变了formatter的值之后,并不影响其他线程初始化的值

ps:创建ThreadLocal时用到了JAVA8的lamada表达式,使用一个新的方法withInitial(),将Supplier功能接口作为参数,相当于

 private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue()
        {
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };

二、ThreadLocal原理

1. thread类源码

public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

Thread类中有一个threadLocalsinheritableThreadLocals变量,都是ThreadLocalMap类型的变量,当ThreadLocal调用set()和get()方法时创建(实际上调用的是ThreadLocalMap的get()和set()方法)

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap

ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocalvalue为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离

ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。

我们还要注意Entry, 它的key是``ThreadLocal<?> k ,继承自`WeakReference, 也就是我们常说的弱引用类型。

2. ThrreadLocal.set()方法源码

首先判断ThreadLocalMap是否存在,如果不存在就创建新的ThreadLocalMap,存在就往ThreadLocalMap中set数据,其中key为ThreadLocal引用,value为设置的值

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

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

主要的核心逻辑还在ThreadLocalMap中

ThreadLocalMap底层结构

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

Entry是继承WeakReference(弱引用)

为什么需要数组呢?没有了链表怎么解决Hash冲突呢?

用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。

解决冲突算法看下面:

3.ThreadLocalMap Hash算法

int i = key.threadLocalHashCode & (len-1);

i就是当前key在散列表中对应的数组下标位置,这里最关键的就是threadLocalHashCode值的计算,ThreadLocal中有一个属性为HASH_INCREMENT = 0x61c88647,它时斐波那契数也叫黄金分割数,好处是分布均匀

图中灰色的是key为null的数据,因为key是弱引用类型,所以垃圾清除会回收key。

那么对于ThreadLocalMap来讲,set()操作需要考虑四种情况

三、ThreadLocal存在问题——内存泄漏

1.ThreadLocalMap Hash冲突

虽然ThreadLocalMap中使用了黄金分隔数来作为hash计算因子,大大减少了Hash冲突的概率,但是仍然会存在冲突。

HashMap中解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树

ThreadLocalMap中并没有链表结构,所以这里不能适用HashMap解决冲突的方式了。

2.ThreadLocalMap.set()详解

总共分为四种情况

第一种情况:通过hash计算后的槽位对应的Entry数据为空:

直接插入

第二种情况:槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致:直接更新

*第三种情况:*槽位数据不为空,往后遍历过程中,在找到Entrynull的槽位之前,没有遇到key过期的Entry

线性查找空余位置插入

*第四种情况:*槽位数据不为空,往后遍历过程中,在找到Entrynull的槽位之前,遇到key过期的Entry,如下图,往后遍历过程中,一到了index=7的槽位数据Entrykey=null

解决方法:探测式清理和启发式清理

四、ThreadLocal使用场景

1.管理Connection

**最典型的是管理数据库的Connection:**当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources"); 

private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations"); private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");

2. 日期排查

SimpleDateFormat类有parse()方法,内部有一个Calendar对象,调用parse()方法会先调用Calendar.clear()方法,然后调用Calendar.add(),如果一个线程先调用add方法,然后另一个线程又调用了clear(),这时候parse()解析的时间就错误。

解决办法不可能让每个线程都new一个单独的SimpleDataFormat,所以使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

ThreadLocalMap getMap(Thread t) { 
	return t.threadLocals; 
	} 
public class Thread implements Runnable {
	…… 
	
	/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ 
	ThreadLocal.ThreadLocalMap threadLocals = null; 
	/*
    * InheritableThreadLocal values pertaining to this thread. This map is
    * maintained by the InheritableThreadLocal class. */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 
    ……

每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离


private void set(ThreadLocal<?> key, Object value) { 
	Entry[] tab = table; int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); 
    for (Entry e = tab[i]; 
    	e != null;
    	e = tab[i = nextIndex(i, len)]) { 
    	ThreadLocal<?> k = e.get(); 
    	
    	if (k == key) { 
    		e.value = value;
    		return; 
    		} 
    	if (k == null) { 
    		replaceStaleEntry(key, value, i); 
    		return; 
    		}
          } 
          tab[i] = new Entry(key, value); 
          int sz = ++size; 
          if (!cleanSomeSlots(i, sz) && sz >= threshold) 				rehash(); 
          }

3.使用MQ发送消息给第三方系统

在MQ发送的消息体中自定义属性requestId,接收方消费消息后,自己解析requestId使用即可

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值