Java面试题必备知识之ThreadLocal
老套路,先列举下关于ThreadLocal常见的疑问,希望可以通过这篇学习笔记来解决这几个问题:
- ThreadLocal是用来解决什么问题的?
- 如何使用ThreadLocal?
- ThreadLocal的实现原理是什么?
- 可否举几个实际项目中使用ThreadLocal的案例?
基础知识
ThreadLocal是线程局部变量,和普通变量的不同在于:每个线程持有这个变量的一个副本,可以独立修改(set方法)和访问(get方法)这个变量,并且线程之间不会发生冲突。
类中定义的ThreadLocal实例一般会被private static
修饰,这样可以让ThreadLocal实例的状态和Thread绑定在一起,业务上,一般用ThreadLocal包装一些业务ID(user ID或事务ID)——不同的线程使用的ID是不相同的。
如何使用
case1
从某个角度来看,ThreadLocal为Java并发编程提供了额外的思路——避免并发,如果某个对象本身是非线程安全的,但是你想实现多线程同步访问的效果,例如SimpleDateFormat,你可以使用ThreadLocal变量。
public class Foo
{
// SimpleDateFormat is not thread-safe, so give one to each thread
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue()
{
return new SimpleDateFormat("yyyyMMdd HHmm");
}
};
public String formatIt(Date date)
{
return formatter.get().format(date);
}
}
注意,这里针对每个线程只需要初始化一次SimpleDateFormat对象,其实跟在自定义线程中定义一个SimpleDateFormat成员变量,并在线程初始化的时候new这个对象,效果是一样的,只是这样看起来代码更规整。
case2
之前在yunos做酷盘项目的数据迁移时,我们需要按照用户维度去加锁,每个线程在处理迁移之前,都需要先获取当前用户的锁,每个锁的key是带着用户信息的,因此也可以使用ThreadLocal变量实现:
case3
下面这个例子,我们定义了一个MyRunnable对象,这个MyRunnable对象会被线程1和线程2使用,但是通过内部的ThreadLocal变量,每个线程访问到的整数都是自己单独的一份。
package org.java.learn.concurrent.threadlocal;
/**
* @author duqi
* @createTime 2018-12-29 23:25
**/
public class ThreadLocalExample {
public static class MyRunnable implements Runnable {
private ThreadLocal<Integer> threadLocal =
new ThreadLocal<Integer>();
@Override
public void run() {
threadLocal.set((int) (Math.random() * 100D));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(threadLocal.get());
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
thread1.join(); //wait for thread 1 to terminate
thread2.join(); //wait for thread 2 to terminate
}
}
ThreadLocal关键知识点
源码分析
ThreadLocal是如何被线程使用的?原理如下图所示:Thread引用和ThreadLocal引用都在栈上,Thread引用会引用一个ThreadLocalMap对象,这个map中的key是ThreadLocal对象(使用WeakReference包装),value是业务上变量的值。
首先看java.lang.Thread
中的代码:
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;
//......其他源码
Thread中的threadLocals变量指向的是一个map,这个map就是ThreadLocal.ThreadLocalMap,里面存放的是跟当前线程绑定的ThreadLocal变量;inheritableThreadLocals的作用相同,里面也是存放的ThreadLocal变量,但是存放的是从当前线程的父线程继承过来的ThreadLocal变量。
在看java.lang.ThreadLocal
类,主要的成员和接口如下:
withInitial方法,Java 8以后用于初始化ThreadLocal的一种方法,在外部调用get()方法的时候,会通过Supplier确定变量的初始值;
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
get方法,获取当前线程的变量副本,如果当前线程还没有创建该变量的副本,则需要通过调用
initialValue
方法来设置初始值;get方法的源代码如下,首先通过当前线程获取当前线程对应的map,如果map不为空,则从map中取出对应的Entry,然后取出对应的值;如果map为空,则调用setInitialValue设置初始值;如果map不为空,当前ThreadLocal实例对应的Entry为空,则也需要设置初始值。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();
}
set方法,跟get方法一样,先获取当前线程对应的map,如果map为空,则调用createMap创建map,否则将变量的值放入map——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);
}
remove方法,删除当前线程绑定的这个副本
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
数字0x61c88647,这个值是HASH_INCREMENT的值,普通的hashmap是使用链表来处理冲突的,但是ThreadLocalMap是使用线性探测法来处理冲突的,HASH_INCREMENT就是每次增加的步长,根据参考资料1所说,选择这个数字是为了让冲突概率最小。
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
父子进程数据共享
InheritableThreadLocal主要用于子线程创建时,需要自动继承父线程的ThreadLocal变量,实现子线程访问父线程的threadlocal变量。InheritableThreadLocal继承了ThreadLocal,并重写了childValue、getMap、createMap三个方法。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* 创建线程的时候,如果需要继承且父线程中Thread-Local变量,则需要将父线程中的ThreadLocal变量一次拷贝过来。
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* 由于重写了getMap,所以在操作InheritableThreadLocal变量的时候,将只操作Thread类中的inheritableThreadLocals变量,与threadLocals变量没有关系
**/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* 跟getMap类似,set或getInheritableThreadLocal变量的时候,将只操作Thread类中的inheritableThreadLocals变量
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
关于childValue多说两句,拷贝是如何发生的?
首先看Thread.init方法,
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
//其他源码
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
然后看ThreadLocal.createInheritedMap方法,最终会调用到newThreadLocalMap方法,这里InheritableThreadLocal对childValue做了重写,可以看出,这里确实是将父线程关联的ThreadLocalMap中的内容依次拷贝到子线程的ThreadLocalMap中了。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
ThreadLocal对象何时被回收?
ThreadLocalMap中的key是ThreadLocal对象,然后ThreadLocal对象时被WeakReference包装的,这样当没有强引用指向该ThreadLocal对象之后,或者说Map中的ThreadLocal对象被判定为弱引用可达时,就会在垃圾收集中被回收掉。看下Entry的定义:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal和线程池一起使用?
ThreadLocal对象的生命周期跟线程的生命周期一样长,那么如果将ThreadLocal对象和线程池一起使用,就可能会遇到这种情况:一个线程的ThreadLocal对象会和其他线程的ThreadLocal对象串掉,一般不建议将两者一起使用。
案例学习
Dubbo中对ThreadLocal的使用
我从Dubbo中找到了ThreadLocal的例子,它主要是用在请求缓存的场景,具体代码如下:
@Activate(group = {Constants.CONSUMER, Constants.PROVIDER}, value = Constants.CACHE_KEY)
public class CacheFilter implements Filter {
private CacheFactory cacheFactory;
public void setCacheFactory(CacheFactory cacheFactory) {
this.cacheFactory = cacheFactory;
}
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (cacheFactory != null && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.CACHE_KEY))) {
Cache cache = cacheFactory.getCache(invoker.getUrl(), invocation);
if (cache != null) {
String key = StringUtils.toArgumentString(invocation.getArguments());
Object value = cache.get(key);
if (value != null) {
if (value instanceof ValueWrapper) {
return new RpcResult(((ValueWrapper)value).get());
} else {
return new RpcResult(value);
}
}
Result result = invoker.invoke(invocation);
if (!result.hasException()) {
cache.put(key, new ValueWrapper(result.getValue()));
}
return result;
}
}
return invoker.invoke(invocation);
}
可以看出,在RPC调用(invoke)的链路上,会先使用请求参数判断当前线程是否刚刚发起过同样参数的调用——这个调用会使用ThreadLocalCache保存起来。具体的看,ThreadLocalCache的实现如下:
package org.apache.dubbo.cache.support.threadlocal;
import org.apache.dubbo.cache.Cache;
import org.apache.dubbo.common.URL;
import java.util.HashMap;
import java.util.Map;
/**
* ThreadLocalCache
*/
public class ThreadLocalCache implements Cache {
//ThreadLocal里存放的是参数到结果的映射
private final ThreadLocal<Map<Object, Object>> store;
public ThreadLocalCache(URL url) {
this.store = new ThreadLocal<Map<Object, Object>>() {
@Override
protected Map<Object, Object> initialValue() {
return new HashMap<Object, Object>();
}
};
}
@Override
public void put(Object key, Object value) {
store.get().put(key, value);
}
@Override
public Object get(Object key) {
return store.get().get(key);
}
}
RocketMQ
在RocketMQ中,我也找到了ThreadLocal的身影,它是用在消息发送的场景,MQClientAPIImpl是RMQ中负责将消息发送到服务端的实现,其中有一个步骤需要选择一个具体的队列,选择具体的队列的时候,不同的线程有自己负责的index值,这里使用了ThreadLocal的机制,可以看下ThreadLocalIndex的实现:
package org.apache.rocketmq.client.common;
import java.util.Random;
public class ThreadLocalIndex {
private final ThreadLocal<Integer> threadLocalIndex = new ThreadLocal<Integer>();
private final Random random = new Random();
public int getAndIncrement() {
Integer index = this.threadLocalIndex.get();
if (null == index) {
index = Math.abs(random.nextInt());
if (index < 0)
index = 0;
this.threadLocalIndex.set(index);
}
index = Math.abs(index + 1);
if (index < 0)
index = 0;
this.threadLocalIndex.set(index);
return index;
}
@Override
public String toString() {
return "ThreadLocalIndex{" +
"threadLocalIndex=" + threadLocalIndex.get() +
'}';
}
}
总结
这篇文章主要是解决了关于ThreadLocal的几个问题:(1)具体的概念是啥?(2)在Java开发中的什么场景下使用?(3)ThreadLocal的实现原理是怎样的?(4)开源项目中有哪些案例可以参考?不知道你是否对这几个问题有了一定的了解呢?如果还有疑问,欢迎交流。
参考资料
- Why 0x61c88647?
- Java ThreadLocal
- When and how should I use a ThreadLocal variable?
- 技术小黑屋:理解Java中的ThreadLocal
- 深入分析ThreadLocal的内存泄漏问题
- 《Java并发编程实战》
- InheritableThreadLocal详解
- ThreadLocal详解
- ThreadLocal的使用场景
- 数据结构:哈希表
本号专注于后端技术、JVM问题排查和优化、Java面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。
Java面试题必备知识之ThreadLocal的更多相关文章
- Java面试题-基础知识
参考文章:Java面试题-基础知识 基础能力 什么是值传递和引用传递 线程状态有哪些,它们之间是如何转换的 进程与线程的区别,进程间如何通讯,线程间如何通讯? HashMap的数据结构是什么?如何实现 ...
- java web开发必备知识
从各种招聘网站的要求上筛选出了一些java开发的一些基本的要求,对照自身看看有哪些缺陷. java基础 既然是java web开发,java SE肯定要学好了. 多线程,IO,集合等,对队列,缓存,消 ...
- Java程序员必备知识-多线程框架Executor详解
为什么引入Executor线程池框架 new Thread()的缺点 每次new Thread()耗费性能 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞 ...
- java面试题之什么是ThreadLocal?底层如何实现的?
ThreadLocal是一个解决线程并发问题的一个类,用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术.但是当我们不想使用同步的时 ...
- Java面试题基础知识(收集)
1.集合类:list和Set比较,各自的子类比较(Arraylist,Vector,inkedLIst,HashSet,TreeSet) List:存入元素有序,元素可以重复,允许null值得存在,主 ...
- Java面试必备知识
JAVA面试必备知识 第一,谈谈final, finally, finalize的区别. 第二,Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类,是否可 ...
- 【面试必备】常见Java面试题大综合
一.Java基础 1.Arrays.sort实现原理和Collections.sort实现原理答:Collections.sort方法底层会调用Arrays.sort方法,底层实现都是TimeSort ...
- JAVA面试题集之基础知识
JAVA面试题集之基础知识 基础知识: 1.C 或Java中的异常处理机制的简单原理和应用. 当JAVA程序违反了JAVA的语义规则时,JAVA虚拟机就 ...
- 超详细的Java面试题总结(四 )之JavaWeb基础知识总结
系列文章请查看: 超详细的Java面试题总结(一)之Java基础知识篇 超详细的Java面试题总结(二)之Java基础知识篇 超详细的Java面试题总结(三)之Java集合篇常见问题 超详细的Java ...
随机推荐
- PHPstudy + phpstrom +xdebug 断点调试(windows) - CSDN博客
原文:PHPstudy + phpstrom +xdebug 断点调试(windows) - CSDN博客 php.ini 配置 需要添加如下内容 [XDebug]xdebug.profiler_ou ...
- 简单的Windows Webcam应用:Barcode Reader
原文:简单的Windows Webcam应用:Barcode Reader 在Windows上用WinForm创建一个Webcam应用需要用到DirectShow.DirectShow没有提供C#的接 ...
- [Leetcode]Single Number && Single Number II
Given an array of integers, every element appears twice except for one. Find that single one. 非常简单的一 ...
- linux 系统中将数据写入文档不能立即保存问题的解决方法
应用场景: 设备跑的是Linux系统,与PC上位机进行通信,上位机可以给Linux发送设备配置信息,Linux将配置信息写入文件中以备设备断电重启时使用. bug现象: 设备正常运行,设备配置信息为A ...
- Wp8 Popup不随输入法偏移问题解决方案
在wp中我们经常要实现,浮窗的效果这时我们就会考虑到Popup,但是在用Popup开发我们会遇到一个非常尴尬的问题,由于Popup不在主界面的可视化树内,在Popup显示的位置在输入法面板出现的范围时 ...
- flask相关使用
一.手动创建一个干净的含有蓝图的flask项目目录 在init.py中 from flask import Flaskmy_app=Flask(__name__)def create_app() ...
- kafka 0.11.0.3 源码编译
首先下载 kafka 0.11.0.3 版本 源码: http://mirrors.hust.edu.cn/apache/kafka/0.11.0.3/ 下载源码 首先安装 gradle,不再说明 1 ...
- Protobuf 小试牛刀
本文以PHP为例. 环境: CentOS 6.8 proto 3.8 PHP 7.1.12 PHP protobuf扩展 3.8.0 go1.12.5 linux/amd64 本文示例仓库地址: ht ...
- IM即时通讯:如何跳出传统思维来设计聊天室架构?
因为视频直播业务的大规模扩张,聊天室这种功能在最近几年又火了起来.本篇文章将会重点挑选聊天室这个典型场景,和大家分享一下网易云信在实现这个功能时是如何做架构设计的. 相关推荐阅读几十万人同时在线的直播 ...
- ZooKeeper —— 单机环境和集群环境搭建
一.单机环境搭建 1.1 下载 下载对应版本Zookeeper,这里我下载的版本3.4.14.官方下载地址:https://archive.apache.org/dist/zookeeper/ # w ...