上节在讨论Thread类的时候,抛出了一个问题,即线程范围之间如何实现数据的共享。其实很简单,利用一个Map来存贮,键存贮线程的名字、id等数据,而值则存贮着该线程对应共享的数据,将该Map传进对应的线程就可以实现数据的共享了,但是得注意同步。防止出现"脏数据"。而ThreadLocal类的存贮策略与上述相似,但是它只保存着每个线程的对应的本地数据,一个线程并不能访问ThreadLocal里另外一个线程保存的数据。说了这么多,还没正式的介绍ThreadLocal类,中文名是本地线程类,该类是用来保存线程的本地数据的,如示例代码:
public class ThreadLocalTest extends Thread {
private ThreadLocal<String> threadLocal;
public ThreadLocalTest(ThreadLocal<String> threadLocal){
this.threadLocal=threadLocal;
}
public void run(){
System.out.println("蕾姆"+threadLocal.get());
}
}
public class Test {
static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static void main(String args[]) throws UnsupportedEncodingException, InterruptedException {
threadLocal.set("拉姆");
System.out.println(threadLocal.get());
ThreadLocalTest threadLocalTest=new ThreadLocalTest(threadLocal);
threadLocalTest.start();
}
//打印:
//拉姆
//蕾姆null
与普通的Map存贮不同,ThreadLocal类的存贮并不需要指明键,因为它默认当前线程为它的键,所以只需要直接set与get即可。从代码中看出,将ThreadLocal类的示例传进另外一个线程,并进行获取,得到的是null值,也就是说ThreadLocal类会保存当前线程的数据,因为ThreadLocalTest 线程没有set进数据,所以在ThreadLocalTest 线程内get不到数据。从这个例子看出,它实现了线程之间的数据的分离,让每个线程都独自管理它的数据,从而不会混淆。它的应用场景是:
关于ThreadLocal类的关键,是其内部的ThreadLocalMap静态内部类,ThreadLocalMap类存贮的节点定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从代码中可以看出,ThreadLocalMap保存的节点继承了WeakReference类,也就是弱引用类,当GC扫描到了该部分时,所有的节点都会被GC回收,但是如果存贮的是较小的单位,那么GC便很小的可能性会扫到,所以ThreadLocal类里面不能存贮较大的数据以及比较重要的数据。每个节点保存着ThreadLocal类以及当前线程的数据。ThreadLocalMap类与HashMap类的主要区别有以下几点:
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
开放地址法是指,当经过计算,某个键的索引值在哈希桶中已经有了节点时,它会将键值对放置那个节点的下一个桶中,如果下一个桶中还有,那么放在下一个桶的下一个桶中,直到存入或者超出桶的上限。如插入的代码:
private void set(ThreadLocal<?> key, Object value) {
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;
//resize桶的大小
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
而为了内存的回收,ThreadLocalMap类里的set、remove、rehash等方法都直接或者间接的调用了expungeStaleEntry方法,该方法是用来将失去键引用的值引用置为null,方便内存回收的:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
而ThreadLocal底层调用的如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//Thread类中获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//创建一个ThreadLocalMap
createMap(t, value);
}
其中ThreadLocalMap 实例是从Thread中得到的,一个线程也只允许一个ThreadLocalMap来保存本地数据,一个ThreadLocalMap类里保存着很多相同的ThreadLocal(this)引用,但是ThreadLocal类对应着很多个ThreadLocalMap,因为每个线程的ThreadLocalMap都不一样,也就是说ThreadLocal类只是个空壳,内部的ThreadLocalMap都是有不同的线程对应的ThreadLocalMap实现的。
关于java.lang包里的与线程相关的类就是ThreadGroup类,ThreadGroup类是线程组类,线程组不同于线程池前者是为了统一管理多个线程的属性,比如设置是否是守护线程,线程的优先级等等。而后者是为了减少连接带来的开销。当你在main方法里创建线程时,那么该线程会自动成为主线程的线程组中的一员。而每一个ThreadGroup都可以包含一组的子线程和一组子线程组,先介绍ThreadGroup的内部成员变量:
public
class ThreadGroup implements Thread.UncaughtExceptionHandler {
当前线程组的父线程组
private final ThreadGroup parent;
//线程组的名字
String name;
//当前线程组最大优先级
int maxPriority;
//是否被销毁
boolean destroyed;
//是否守护线程
boolean daemon;
//是否可以中断
boolean vmAllowSuspension;
//当前线程组的线程数量
int nthreads;
//存贮当前的线程
Thread threads[];
//当前线程组的数量
int ngroups;
//存贮多个线程组
ThreadGroup groups[];
}
如何让线程加入指定的线程组呢?其实很简单,在创建此线程的时候,指定对应的线程组就行了。通常情况下我们创建线程时可能不设置线程组,这时候创建的线程会和创建该线程的线程所在组在一个组里面。一般来说,在main方法中创建的线程,它的父线程组是main线程组,而main线程组的父线程组是system线程组,如下代码:
System.out.println(Thread.currentThread().getThreadGroup().getName());
System.out.println(Thread.currentThread().getThreadGroup().getParent().getName());
System.out.println(Thread.currentThread().getThreadGroup().getParent().getParent().getName();
//打印:main
//system
//报异常
由此可见,system是最高级别的线程组了。因为是对线程组里所有线程的操作,所以ThreadGroup类的操作大多是批处理操作,具体有如下:
//设置是否守护线程
public final void setDaemon(boolean daemon);
//设置最高优先级
public final void setMaxPriority(int pri);
//线程组的内的活跃线程数
public int activeCount();
//将线程组内的线程复制给list线程数组
public int enumerate(Thread list[]);
//中断处于阻塞的线程
public final void interrupt();
还需注意的是ThreadGroup 类实现了Thread.UncaughtExceptionHandler接口,也就是说ThreadGroup 可以设置未处理异常的处理方法,当线程组中某个线程发生Unchecked exception异常时,由执行环境调用此方法进行相关处理,如果有必要,可以重新定义此方法,如下面的示例:
public class GroupTest extends ThreadGroup {
public GroupTest(String name) {
super(name);
}
public void uncaughtException(Thread t, Throwable e){
//异常在这里处理
System.out.println("蕾姆在这里处理异常啦");
}
}
GroupTest groupTest=new GroupTest("蕾姆");
Thread thread=new Thread(groupTest, new Runnable() {
@Override
public void run() {
throw new NullPointerException();
}
});
thread.start();
ThreadGroup 类默认的处理未捕获异常的方式是,先判断是否有父线程组,如果有,交给父线程组处理,若没有就采取自身默认的处理方式处理:
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}