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 formatter = new ThreadLocal(){
@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 threadLocal =
new ThreadLocal();
@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
ThreadLocalwithInitial(Supplier 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 extends ThreadLocal {
/**
* 创建线程的时候,如果需要继承且父线程中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