java.lang.ThreadLocal

JDK解析

        public class ThreadLocal<T> extends Object

        该类提供了线程局部(thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

       例如,以下类生成对每个线程唯一的局部标识符。线程 ID 是在第一次调用UniqueThreadIdGenerator.getCurrentThreadId()时分配的,在后续调用中不会更改。

import java.util.concurrent.atomic.AtomicInteger;

public class UniqueThreadIdGenerator {
private static final AtomicInteger uniqueId = new AtomicInteger(0);

private static final ThreadLocal<Integer> uniqueNum = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
         		return uniqueId.getAndIncrement();
        }
};

public static int getCurrentThreadId() {
     	return uniqueId.get();
    	}
} // UniqueThreadIdGenerator

        每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且ThreadLocal实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

 

ThreadLocal概述与源码分析

        ThreadLocal的作用可以总结为一句话,它是提供给线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

 

首先,在Thread类中有一行

/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

其中ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中。同时,ThreadLocalMap中用于存储数据的entry定义:

static class ThreadLocalMap {
    /**
    The entries in this hash map extend WeakReference, using its main ref field as the key (which is always a ThreadLocal object).  Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, so the entry can be expunged from table.  Such entries are referred to as "stale entries" in the code that follows.
    */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

从中我们可以发现这个Map的key是ThreadLocal类的实例对象,value为用户的值,而并不是线程的名字或者标识。ThreadLocal的set和get方法代码:

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();
}

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

其中的getMap方法,给当前Thread类对象初始化ThreadlocalMap属性:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

简单解析一下,get方法的流程是这样的:

        1、首先获取当前线程;

        2、根据当前线程获取一个Map

        3、如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5

        4、如果e不为null,则返回e.value,否则转到5

       5Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKeyfirstValue创建一个新的Map

        类似的,当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value。

若没有给ThreadLocal变量设置值,则返回它的初始值:

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
     	map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

用户可以覆盖initialValue()方法,设置默认的初始值。

 

        到这里,我们就可以理解ThreadLocal究竟是如何工作的了:

        1、Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象。

        2、ThreadLocal变量的set与get方法都通过ThreadLocalMap来执行。

        3、ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。

        4、由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的,下面给出一个例子:
import java.util.Random;
/**
 * 
 * @Description: 线程范围内的数据共享
 *
 * @author: zxt
 *
 * @time: 2018年4月6日 下午10:11:14
 *
 */
public class ThreadScopeDataShare2 {
	// 使用ThreadLocal来保存不同线程的局部变量副本
	private static ThreadLocal<String> threadLocalData = new ThreadLocal<String>();
	
	public static void main(String[] args) {
		// 新建两个线程(需要保证这两个线程之间的数据不会相互影响)
		for(int i = 0; i < 2; i++) {
			new Thread(new Runnable() {
				
				public void run() {
					int putData = new Random().nextInt();
					
					// 将线程的数据放入到ThreadLocal中
					threadLocalData.set(Thread.currentThread().getName() + ", " + putData);
					System.out.println(Thread.currentThread().getName() + " has put data:" + putData);
					
					// 在同一个线程中,不同的对象取数据
					A.get();
					B.get();
				}
				
			}).start();
		}
	}
	
	static class A {
		public static void get() {
			// 获取线程在ThreadLocal中保存的变量
			String data = threadLocalData.get();
			System.out.println("A from " + Thread.currentThread().getName() + " get data:" + data);
		}
	}
	
	static class B {
		public static void get() {
			// 获取线程在ThreadLocal中保存的变量
			String data = threadLocalData.get();
			System.out.println("B from " + Thread.currentThread().getName() + " get data:" + data);
		}
	}
}

由运行结果可以发现,两个线程之间的数据不会相互影响。


        由上面的分析也可以发现,一个ThreadLocal对象只能保存线程的一个局部变量,若有多个数据需要保存,则只能定义多个ThreadLocal的变量。当然也可以将需要保存的多个变量封装在一个类中,而把封装类保存到ThreadLocal中。实例如下:

package com.zxt.ThreadScopeDataShare;

import java.util.Random;

/**
 * 
 * @Description: 线程范围内的数据共享
 *
 * @author: zxt
 *
 * @time: 2018年4月6日 下午10:11:14
 *
 */
public class ThreadScopeDataShare3 {

	public static void main(String[] args) {
		// 新建两个线程(需要保证这两个线程之间的数据不会相互影响)
		for (int i = 0; i < 2; i++) {
			new Thread(new Runnable() {

				public void run() {
					int putData = new Random().nextInt();

					// 将线程的数据放入到ThreadLocal中
					MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
					myData.setName(Thread.currentThread().getName());
					myData.setData(putData);
					System.out.println(Thread.currentThread().getName() + " has put data:" + myData);

					// 在同一个线程中,不同的对象取数据
					A.get();
					B.get();
				}

			}).start();
		}
	}

	static class A {
		public static void get() {
			// 获取线程在ThreadLocal中保存的变量
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("A from " + Thread.currentThread().getName() + " get data:" + myData);
		}
	}

	static class B {
		public static void get() {
			// 获取线程在ThreadLocal中保存的变量
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("B from " + Thread.currentThread().getName() + " get data:" + myData);
		}
	}
}

/**
 * 
 * @Description: 封装线程需要保存的局部变量,将构造函数设置为私有,防止外部创建,只有在将数据保存到ThreadLocal的时候创建
 * 				  并且其操作时,先判断是否已经保存过该类型的数据,若保存过,则将该实例返回,直接修改即可,若没有,则先创建,再填数据
 *
 * @author: zxt
 *
 * @time: 2018年4月7日 下午8:09:56
 *
 */
class MyThreadScopeData {

	private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();

	private MyThreadScopeData() {
	}

	public static MyThreadScopeData getThreadInstance() {
		MyThreadScopeData instance = map.get();
		if (instance == null) {
			instance = new MyThreadScopeData();
			map.set(instance);
		}

		return instance;
	}

	private String name;
	private int data;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getData() {
		return data;
	}

	public void setData(int data) {
		this.data = data;
	}
	
	public String toString() {
		return name + ":" + data;
	}
}


为什么不直接用线程id来作为ThreadLocalMap的key?

        这一点很容易理解,因为直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value。比如我们放入了两个字符串,你如何知道我要取出来的是哪一个字符串呢?

        而使用ThreadLocal作为key就不一样了,由于每一个ThreadLocal对象都可以由threadLocalHashCode属性唯一区分或者说每一个ThreadLocal对象都可以由这个对象的名字唯一区分,所以可以用不同的ThreadLocal作为key,区分不同的value,方便存取。

        同时相比于用线程id作为key值,这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能。并且当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

 

ThreadLocal的内存泄露问题

        根据上面Entry方法的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。下图是相关对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

        如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

        Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄露。

        ThreadLocalMap设计时的对上面问题的对策:

        ThreadLocalMap的getEntry函数的流程大概为:

        1、首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode& (table.length-1)运算得到)获取Entrye,如果e不为null并且key相同则返回e;

        2、如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。

        但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成privatestatic的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

 

Synchronized与ThreadLocal的比较

        synchronized和使用ThreadLocal均可以解决以上的问题,只是这是两种不同的方式,synchronized是依赖锁的机制一个执行完后另一个再执行。ThreadLocal会为每一个线程维护一个和该线程绑定的变量的副本,从而隔离了多个线程的数据,每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。

        概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

        当然ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量),这样当然不需要对多个线程进行同步了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值