1 概述
变量值的共享可以使用public static 的声明方式,所有的线程都是用同一个public static变量,那如果想实现每一个线程都有自己的变量该如何解决呢?JDK提供的ThreadLocal就派上用场了。
ThreadLocal类主要的作用就是将数据放入当前线程对象中的Map里,这个Map类是Thread类的实例变量。ThreadLocal类自己不管理也不存储任何数据,它只是数据和Map之间的中介和桥梁,通过ThreadLocal将数据放入Map中,执行流程如下:
数据——>ThreadLocal——>currentThread()——>Map
执行后每个线程中的Map就存储自己的数据,Map中的key存储的是ThreadLocal对象,value就是存储的值,说明ThreadLocal和值之间是一对一的关系,一个ThreadLocal对象只能关联一个值。每个线程中Map的值只对向前线程可见,其他线程不可以访问当前线程对象中Map的值。内存结构如下:
2 get()方法与null
如果从未在Thread中的Map存储 ThreadLocal对象对应的值,则get()方法返回null。
public class Run1 {
public static ThreadLocal t1 = new ThreadLocal();
public static void main(String[] args) {
if(t1.get() == null){
System.out.println("从未放过值");
t1.set("第一次放的值");
}
System.out.println(t1.get());
System.out.println(t1.get());
}
}
ThreadLocal类解决的是变量在不同线程中的隔离性,也就是不同线程拥有自己的值,不同线程中的值是可以通过ThreadLocal类进行保存的。
3 ThreadLocal类存取数据流程分析
运行测试程序:
public class Test {
public static void main(String[] args) {
ThreadLocal local = new ThreadLocal();
local.set("value");
System.out.println(local.get());
}
}
从JDK源码角度来分析一下ThreadLocal类执行存取操作的流程。
首先看一下数据如何存入到ThreadLocal中的。
(1)执行ThreadLocal.set("value")代码时,ThreadLocal代码如下:
public void set(T value) {
//对象t就是main线程
Thread t = Thread.currentThread();
//从main线程中获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果map不等于null,则set操作
map.set(this, value);
} else {
//如果map等于null,则先执行创建,在执行set
createMap(t, value);
}
}
(2)ThreadLocalMap map = getMap(t); 源码如下:
ThreadLocalMap getMap(Thread t) {
//参数t就是前面传入的main线程
return t.threadLocals;//返回main线程中threadLocals变量对应的ThreadLocalMap对象
}
对象threadLocals数据类型就是ThreadLocal.ThreadLocalMap,变量threadLocals是Thread类中的实例变量。
(3)取得Thread中的ThreadLocal.ThreadLocalMap后,根据map对象值是不是null来决定是否对其执行set或create and set操作。
(4)createMap()方法的功能是创建一个新的ThreadLocalMap,并在这个新的ThreadLocalMap里存储数据,ThreadLocalMap中的key就是当前ThreadLocal对象,值就是传入的value,createMap()方法的源码如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
在实例化ThreadLocalMap的时候,向构造方法传入thi和firstValue,其中,this就是当前ThreadLocal对象,firstValue就是调用ThreadLocal对象时set()方法传入的参数值。
new ThreadLocalMap(this, firstValue)的源码是:
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);
}
在源码中可以发现,将ThreadLocal对象与firstValue封装进Entry对象中,并放入table[]数组。
再看一下get()的执行流程。
(1)当执行 local.get() 代码时,ThreadLocal.get()源码如下:
public T get() {
Thread t = Thread.currentThread();//t 就是main线程
ThreadLocalMap map = getMap(t);//从main线程中获取ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//如果map不等于null,获取Entry对象
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
总结:上面的几个步骤就是set和get的执行流程,比较麻烦。为什么不能直接向Thread类中的ThreadLocalMap对象存取数据呢?这是无法实现的,原因参考下面代码:
ThreadLocal.ThreadLocalMap threadLocals = null;
变量 threadLocals 默认是包级访问,所以不能从外部直接访问该变量,也没有对应的get和set方法,只有用同一个包中的类可以访问threadLocals变量,而ThreadLocal和Thread恰好在同一个包中(都在java.lang包下)。
4 验证线程变量的隔离性
本节将实现通过使用ThreadLocal在每个线程中存储自己的私有数据。
public class Tools {
public static ThreadLocal t1 = new ThreadLocal();
}
public class MyThreadA extends Thread{
@Override
public void run(){
try {
for (int i = 0; i < 10; i++) {
Tools.t1.set("A: " +(i+1) );
System.out.println("A get:" + Tools.t1.get());
int sleepValue = (int)(Math.random() * 10000);
Thread.sleep(sleepValue);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class MyThreadB extends Thread{
@Override
public void run(){
try {
for (int i = 0; i < 10; i++) {
Tools.t1.set("B: " +(i+1) );
System.out.println("B get:" + Tools.t1.get());
int sleepValue = (int)(Math.random() * 10000);
Thread.sleep(sleepValue);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class Run1 {
public static void main(String[] args) throws InterruptedException {
MyThreadA a = new MyThreadA();
MyThreadB b = new MyThreadB();
a.start();
b.start();
for (int i = 0; i < 10; i++) {
Tools.t1.set("main: " +(i+1) );
System.out.println("main get:" + Tools.t1.get());
int sleepValue = (int)(Math.random() * 10000);
Thread.sleep(sleepValue);
}
}
}
控制台输出的结果表示通过ThreadLocal向每个线程存储自己的私有数据,虽然3个线程都向t1存放数据,但是每个线程仅能取出自己的数据,不能取出其他线程存放的数据 。
5 解决get()返回null的问题
新建ThreadLocalExt.java,继承ThreadLocal类,并覆盖 initialValue() 方法
public class ThreadLocalExt extends ThreadLocal{
@Override
protected Object initialValue(){
return "我是默认值,第一次get不再为null";
}
}
覆盖initialValue()方法具有初始值,因为ThreadLocal.java中的initialValue方法默认返回值就是null,所以要在子类中重写。源码如下:
protected T initialValue() {
return null;
}
public class Run1 {
public static ThreadLocalExt t1 = new ThreadLocalExt();
public static void main(String[] args) {
if(t1.get() == null){
System.out.println("没有存放过值");
t1.set("第一次存放值");
}
System.out.println(t1.get());
System.out.println(t1.get());
}
6 验证重写initialValue()方法的隔离性
public class Tools {
public static ThreadLocalExt t1 = new ThreadLocalExt();
}
public class ThreadLocalExt extends ThreadLocal{
@Override
protected Object initialValue() {
return new Date().getTime();
}
}
public class ThreadA extends Thread{
@Override
public void run(){
try {
for (int i = 0; i < 10; i++) {
System.out.println("在线程ThreadA中取值 = " + Tools.t1.get());
Thread.sleep(100);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class Run1 {
public static void main(String[] args) {
try {
for (int i = 0; i < 10; i++) {
System.out.println("在Main线程中取值 = " + Tools.t1.get());
}
Thread.sleep(2000);
ThreadA threadA = new ThreadA();
threadA.start();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
7 使用remove()方法的必要性
ThreadLocalMap中的静态内置类Entry是弱引用类型,源码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用的特点是,只要垃圾回收器扫描时发现弱引用的对象,就不管内存是否足够,都会回收弱引用的对象。也就是只要执行gc操作,ThreadLocal对象就会立即销毁,代表key的值ThreadLocal对象会随着gc操作而销毁,释放内存空间,但value值却不会随着gc操作而销毁,这会出现内存溢出。如果对象数量过多,对于ThreadLocalMap类中不用的数据使用ThreadLocal类的remove方法进行清除,实现业务对象的垃圾回收,释放内存。