Java并发——ThreadLocal、ThreadGroup类解析

版权声明:博主GitHub地址https://github.com/suyeq欢迎大家前来交流学习 https://blog.csdn.net/hackersuye/article/details/84592249

ThreadLocal

    上节在讨论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不到数据。从这个例子看出,它实现了线程之间的数据的分离,让每个线程都独自管理它的数据,从而不会混淆。它的应用场景是:

  1. 方便同一个线程使用某一对象,避免不必要的参数传递;
  2. 线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);

    关于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类的主要区别有以下几点:

  1. HashMap存贮的是强引用,而ThreadLocalMap类存贮的是弱引用;
  2. HashMap默认的负载因子是0.75,且可以从外部改变该值,而ThreadLocalMap类的默认是2/3,且不能更改,HashMap类的threshold 大于桶长度,而ThreadLocalMap类小于桶长度;
private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
  1. HashMap解决键冲突是采用链表法解决,而ThreadLocalMap类则是采取开放地址法来解决:
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实现的。

ThreadGroup

    关于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);
            }
        }
    }

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/84592249