ThreadLocal的基本使用
ThreadLcoal位于JDK的java.lang核心包中.如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值的时候,每个线程都会有一个独立的 自己的本地值.再多线程并发操作本地变量的时候,线程各自操作自己本地的值,从而避免线程安全问题.
方法 | 说明 |
set(T value) | 设置当前线程在"线程本地变量"实例中绑定的值 |
T get() | 获取当前线程在"线程本地变量"实例中绑定的值 |
remove() | 移除当前线程在"线程本地变量"实例中绑定的值 |
public class ThreadLocalDemo1 {
@Data
static class Local {
//实例总数.
static final AtomicInteger AMOUNT = new AtomicInteger(0);
//对象的编号.
int index = 0;
//对象的内容.
int bar = 10;
//构造器.
public Local() {
index = AMOUNT.incrementAndGet();
}
@Override
public String toString() {
return index + "@Local{bar" + bar + "}";
}
}
private static final ThreadLocal<Local> Local = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
if (Local.get() == null) {
Local.set(new Local());
}
System.out.println("线程初始本地值是" + Local.get());
for (int i1 = 0; i1 < 10; i1++) {
Local local = Local.get();
local.setBar(local.getBar() + 1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("累加十次线程本地值是" + Local.get());
Local.remove();
}
});
}
threadPool.shutdown();
}
}
通过运行这个例子可以看出,在多线程下,threadLocal的结果并没有出现线程不安全的现象.ThreadLocal还有一个静态工厂方法
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
在定义ThreadLocal对象时设置一个初始的回调函数.
ThreadLocal<Local> localThreadLocal =
ThreadLocal.withInitial(() -> new Local());
这种用法通过调用工厂方法创建了一个ThreadLocal对象,并传递了一个获取初始值的Lambda回调函数.我建议大家可以去看看这个方法的实现.相信这个彩蛋会让你有意想不到的发现.
ThreadLcoal使用场景
1:线程隔离
ThreadLcoal的主要价值在于线程隔离,其中的数据只属于当前线程,本地值对其他线程是不可见的,多线程环境下,可以防止自己的变量被其他线程篡改.由于各个线程之间的数据隔离,避免了同步加锁带来的性能损失,提升了并发的性能.
线程隔离使用案例
每个线程绑定一个用户会话信息 数据库连接 http请求等.这样一个线程所有调用到的处理函数都可以很方便的访问这些资源.
使用场景
数据库连接独享 Session数据管理等.
2:跨函数传递数据
通常用于同一个线程内,跨类 跨方法传递数据时,如果不用ThreadLcoal,那么相互之间传递数据势必要靠返回值和参数,无形之间增加了类或方法之间的耦合度.
由于ThreadLocal的特性,同一线程在某些地方进行设置,再随后的任意地方都可以获取.线程执行过程中所涉及到的函数都能读写线程本地值,从而方便的实现跨函数的数据传递.
使用场景
可以为每一个线程绑定一个Session(用户会话)信息,一个线程所访问的地方可以很方便的访问这个本地会话.
ThreadLocal内部结构演进
在早期的JDK版本中ThreadLocal内部结构是一个Map,其中线程实例为Key,线程绑定的线程本地变量为Value.这个Map的拥有者为ThreadLocal实例.每一个实例拥有一个Map实例.
大部分应用中,线程可能会配置几百个,本地变量也就配置几个.
了解过HashMap的话,会知道在扩容的时候存在高成本低性能的问题.因为HashMap内部是一个槽位(slot)数组,这个数组也叫哈希表,存储的是key的哈希值,当槽位数组中的元素个数超过默认容量(16)乘以加载因子(0.75)的时候会进行扩容,扩为容量为32的数组.对于每一个槽位,可以理解为桶(bucket),如果一个桶内元素超过8个,链表会变成红黑树,这是都是高性能低成本的工作.
ThreadLocal实例内部的Map结构叫做ThreadLcoalMap,没有直接采用HashMap对象,而是自定义的和HashMap类似的结构,与HashMap不同之处在于ThreadLcoalMap去掉了桶结构,如果发生哈希碰撞,将key相同的Entry放在槽位后面相邻的空闲位置.HashMap(数组加链表)的处理方式叫做链地址法,发生碰撞就把Entry放在链表中.ThreadLcoalMap的叫做开放地址法,即发生碰撞,就按照某种方法继续寻找其他存储单元,直到找到空位置.
ThreadLocalMap和HashMap一样存在扩容的问题,在线程比较多局部变量比较少的场景下是不是可以转换思路,将ThreadLcoal实例变成Key,一个线程一个Map,这样可以避免扩容的性能消耗.
在JDK8中ThreadLcoal内部结构发生了变化,还是使用Map结构,但是拥有者发生了变化,拥有者为Thread实例,每个Thread实例拥有一个Map实例,Map结构的key也发生了变化,变成了ThreadLcoal实例.
然后每一个Thread线程内部都有一个Map(ThreadLcoalMap),如果我们给一个线程创建了多个ThreadLocal实例,然后放置本地数据,线程的ThreadLocalMap中就会有多个key-value,ThreadLocal为key,本地变量为Value.
早期版本与新版本变化
1:拥有者发生变化:新版本的ThreadLocalMap拥有者为Thread,早期版本为ThreadLocal.
2:key发生了变化:新版本的key为ThreadLcoal实例,早期为Thread实例.
新版本的优势
1:每个ThreadLocalMap存储的key-value数量变少.早期版本的key-value数量和线程强关联,若线程数量多,ThreadLcoalMap存储的key-value数量也多.新版本的key为ThreadLocal实例,多线程情况下,ThreadLcoal实例少于线程数.
2:早期版本ThreadLcoalMap的拥有者为ThreadLocal,在Thread线程被销毁后,ThreadLocalMap还是存在的,新版本的拥有者为Thread,当Thread实例被销毁后,ThreadLocalMap也会随之销毁,减少内存的消耗.