一 简介
面试问,synchronized和ThreadLocal的区别是什么?
这个问题很好理解,synchronized是jvm对于多线程并发情况下提供的一种互斥锁的机制,而Threadlocal是为解决多线程程序并发提供的一个新思路,
Threadlocal为每一个使用该变量的线程提供独立的变量副本,所以每一个线程都能独立地改变自己的副本,从而不影响其他线程对应的副本数据
二 源码
使用ThreadLocal
/**
* Created by zhoucg on 2019-09-12.
*
* ThreadLocal 实例使用
*/
public class ThreadLocalPoolExample {
public static class MyRunnable implements Runnable {
/**
* 实例对象
*/
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
@Override
public void run() {
threadLocal.set( (int) (Math.random() * 100D) );
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
}
}
public static void main(String[] args) throws Exception {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
代码中,首先是定义了一个公共的Runnable类,并启动两个线程执行对应的线程方法,在Runnable对象中,定义了一个ThreadLocal对象,最终的执行结果
下面就开始进入到ThreadLocal的源码中,看一看对应的ThreadLocal的底层实现
ThreadLocal中,公共的方法就5个
1,ThreadLocal();提供无参构造函数创建ThreadLocal对象
2,T get();获取当前线程的变量值
3,void remove();删除当前线程持有的副本的值
4,void set(T value);设置当前线程持有的变量值:Sets the current thread’s copy of this thread-local variable to the specified value
5,ThreadLocal<> withInitial(Supplier<? extends S> supplier);通过传入Supplire对象的方法创建对应的ThreadLocal,内部实际上是返回SuppliedThreadLocal类型的对象,这个类是ThreadLocal的内部类
ThreadLocal中,提供了两个内部类
SuppliedThreadLocal:继承ThreadLocal,接收Supplier对象,实现了ThreadLocal中的initialValue()方法
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
ThreadLocalMap:实际的内部变量副本存储
首先,我们进入到ThreadLocal的set(T value);方法,设置当前线程副本变量的值
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set(T value)中,首先获取当前线程对象,然后通过getMap(t),获取当前线程对象持有的ThreadLocal的内部类ThreadLocalMap对象,
实际上,每一个Thread对象中,都持有了一个Thread.ThreadLocalMap变量,这个可以从Thread源码中看到
ThreadLocal.ThreadLocalMap threadLocals = null;
这个就是为什么ThreadLocal能够保证每个线程都持有变量的副本,并且线程之间互不影响的原因,
偶然间看到网上提出了这么一个问题
下面的博主回答的是正确的,我们在使用ThreadLocal的时候,是会出现一个线程使用多个ThreadLocal进行数据存储的场景的,这个也是ThreadLocalMap中使用数组进行存储数据的原因,当当前线程不存在指定ThreadLocalMap的时候,threadLocals为null的时候,set方法中,首先进行createMap(t,value)操作,创建一个新的ThreadLocalMap并赋值给指定线程的threadLocals变量
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
创建ThreadLocalMap时,传递当前ThreadLocal对象作为key,存储的结果作为value,ThreadLocalMap构造函数中,首先是创建table数组,数组的默认长度设置为16,然后获取key的threadLocalHashCode值和对应的数组长度-1进行与&操作,实际上就是取模,我们来重点关注一下是如何获取threadLocalHashCode值的
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
实际上是通过原子操作类AtomicInteger进行getAndAdd()操作获取的,这里有一个神奇的事情,就是每次创建ThreadLocal实例threadLocalHashCode值得时候,都是会累加0x61c88647
/**
* 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;
注解翻译的意思就是连续生成的哈希代码之间的差异,将生成的哈希代码能够均匀连续得分布在2的N次方的数组
这个魔数得选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527
通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。。为了优化效率。
如果当前线程存在threadLocals变量时,调用ThreadLocalMap的set(ThreadLocal<?> key,Object value)方法
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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();
}
首先是通过int i = key.threadLocalHashCode & (len-1);获取当前ThreaLocal存放在数组的位置,如果当前位置存在数据,ThreadLocalMap使用的是线性探测法查找下一个位置是否存在对应的值,不存在,则进行存在,这个和HashMap中使用链表法是不一样的
ThreadLocalMap对于扩容的操作是,首先清理掉数组中过期的Entry,并且判断对应数据容量的值达到默认阈值的时候调用rehash()。默认的阈值为当前数组的容量乘上3分之2
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
rehash()方法中,jdk1.8中,又进行了进一步的判断,当前容量大于阈值减去阈值的四分之一的时候,再继续扩容
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
扩容的策略就是新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,主要注意的是,在扩容的过程中针对脏entry的话会令value为null,以便能够被垃圾回收器能够回收,解决隐藏的内存泄漏的问题。
ThreadLocal中的get方法实际上就是通过ThreadLocal对象取模获取当前线程threads变量中ThreadLocalMap数组中的值
ThreadLocal中的remove方法通过往后环形查找到与指定key相同的entry后,先通过clear方法将key置为null后,使其转换为一个脏entry,然后调用expungeStaleEntry方法将其value置为null,以便垃圾回收时能够清理,同时将table[i]置为null。
三 疑问
有一个小小的疑问,为什么ThreadLocal中使用ThreadLocalMap进行对应的数据存储,并且设置了默认16位的数组长度,其实在大部分的开发中,我们并不会在一个线程中同时副本出超过16个ThreadLocal对象,也就是一两个,那在多线程的开发中,因为每一个线程都持有ThreadLocalMap,不是存在浪费内存的情况吗?或许可以通过参数的控制设置默认的ThreadLocalMap数组的长度。
应该是Josh Bloch and Doug Lea两位顶级大佬的开发思路更高级,为了设置更通用的工具类进行设计而成的吧,