线程组和 ThreadLocal你应该知道的事

640?wx_fmt=png


今日科技快讯


近日,据多家外媒报道,谷歌向中国电商京东投资5.5亿美元。 京东发布公告称,Google和京东将在一系列战略项目上进行合作,其中一项是在包括东南亚、美国和欧洲在内的全球多个地区合作开发零售解决方案。而这笔投资是双方战略合作伙伴的一部分。 


作者简介


又至周五,又至放假,提前祝大家周末愉快。

本篇来自 指点 的投稿,分享了他结合源码对 java 多线程的理解,一起来看看!希望大家喜欢。

指点 的博客地址:

https://blog.csdn.net/hacker_zhidian


前言


在之前的文章中,我们从源码的角度上解析了一下线程池,并且从其 execute 方法开始把线程池中的相关执行流程过了一遍。那么接下来,我们来看一个新的关于线程的知识点:线程组。


线程组ThreadGroup


我们前面已经讲了线程池,并且我们知道线程池是为了在子线程中处理大量的任务,同时又避免频繁的创建和销毁线程带来的系统资源开销而产生的。 

那么线程组呢?线程组可以说是为了方便和统一多个线程的管理而产生的。我们知道,在一个 Java 程序运行的时候会默认创建一个线程,我们称其为主线程,即为执行 main 方法的线程。其实,在一个 Java 程序运行的时候也会创建一个线程组,而这个主线程正是属于这个线程中的。我们来通过例子看一下:

/**
 * 线程组测试
 */

public static class ThreadGroupTest {

    // 获取主线程所在线程组,在主线程中执行
    public static void printMainThreadGroup() {
        // 获取当前线程所在的线程组对象
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        System.out.println(Thread.currentThread().getName() + 
            " 线程所在的线程组:" + group.getName());
    }

    public static void startTest() {
        printMainThreadGroup();
    }
}

public static void main(String[] args) {
    ThreadGroupTest.startTest();
}

来看看结果:

640?wx_fmt=png

可以看到,主线程所属的线程组名字为 main 。其实,线程组中不仅可以包含线程,也可以包含线程组,这个有点类似于文件夹的概念,线程对应的就是文件,线程组对应的就是文件夹,文件夹中不仅可以包含文件,也可以包含文件夹。这个过程可以用下面的图来表示:

640?wx_fmt=png

我们来看一下 ThreadGroup 类的声明: 

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    private final ThreadGroup parent;
    String name;
    int maxPriority;
    boolean destroyed;
    boolean daemon;
    boolean vmAllowSuspension;

    int nUnstartedThreads = 0;
    int nthreads;
    Thread threads[];

    int ngroups;
    ThreadGroup groups[];

   /**
     * Constructs a new thread group. The parent of this new group is
     * the thread group of the currently running thread.
     * <p>
     * The <code>checkAccess</code> method of the parent thread group is
     * called with no arguments; this may result in a security exception.
     *
     * @param   name   the name of the new thread group.
     * @exception  SecurityException  if the current thread cannot create a
     *               thread in the specified thread group.
     * @see     java.lang.ThreadGroup#checkAccess()
     * @since   JDK1.0
     */

    public ThreadGroup(String name) {
        this(Thread.currentThread().getThreadGroup(), name);
    }
    // ...

可以看到 ThreadGroup 类中包含一些的状态保存字段,包括:所属父线程组(parent),名字(name),其中线程的最大优先级(maxPriority),是否已经被销毁(destroyed),是否为守护线程组(daemon)……

下面来看一下 ThreadGroup 中常用的方法:

// 创建一个指定名称(name)的新线程组,以调用这个构造方法的线程所在的线程组作为父线程组
ThreadGroup(String name) 

//......
void suspend() 

需要注意的是,当你新建一个线程 / 线程组之后,如果你没有给这个新建的线程 / 线程组指定一个父线程组,那么其默认会将当前执行创建线程 / 线程组代码的线程所属的父线程组作为新的线程 / 线程组的父线程组。 同时,一个线程只有调用了其 start 方法之后,其才真正算是被添加到了对应的线程组中。对于这个,可以参考以下源码。

Thread.java 中的 init 方法内:

// ...
g.addUnstarted();
// ...

这个方法会在 Thread 类的init 方法中调用,而 Thread 的构造方法又会调用 init 方法,即 Thread 的构造方法会调用当前方法,其中 g 为当前线程所属的线程组。意为添加未开始的线程。接下来看看 addUnstarted 方法。

ThreadGroup.java: 

/**
 * Increments the count of unstarted threads in the thread group.
 * Unstarted threads are not added to the thread group so that they
 * can be collected if they are never started, but they must be
 * counted so that daemon thread groups with unstarted threads in
 * them are not destroyed.
 */

void addUnstarted() {
    synchronized(this) {
        if (destroyed) {
            throw new IllegalThreadStateException();
        }
        nUnstartedThreads++;
    }
}

可以看到,对于没有调用 start 方法的线程,其所属的线程组只是把 nUnstartedThreads 值加一,并没有真正的添加,我们再来看 Thread.start() 方法: 

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */

        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */

            }
        }
    }

在其中调用了 group.add 方法,不用说我们也知道 group 为当前线程所属的线程组,再看看 ThreadGroup.add 方法:

void add(Thread t) {
    synchronized (this) {
        if (destroyed) {
            throw new IllegalThreadStateException();
        }
        if (threads == null) {
            threads = new Thread[4];
        } else if (nthreads == threads.length) {
            threads = Arrays.copyOf(threads, nthreads * 2);
        }
        threads[nthreads] = t;

        // This is done last so it doesn't matter in case the
        // thread is killed
        nthreads++;

        // The thread is now a fully fledged member of the group, even
        // though it may, or may not, have been started yet. It will prevent
        // the group from being destroyed so the unstarted Threads count is
        // decremented.
        nUnstartedThreads--;
    }
}

可以看到,到这里才是真正的将线程加入线程组中。

从 ThreadGroup 类提供的 API 方法来看,其注重的更多是实现对多个线程的管理,而线程池注重的是利用多个线程执行大量任务。我们来看一个简单的例子,通过线程组来批量停止其中的线程:

/**
 * 线程组测试
 */

public static class ThreadGroupTest {

    // 获取主线程所在线程组,在主线程中执行
    public static void printMainThreadGroup() {
        // 获取当前线程所在的线程组对象
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        System.out.println(Thread.currentThread().getName() + 
                " 线程所在的线程组:" + group.getName());
    }

    // 通过 ThreadGroup 批量停止线程
    public static void stopThreadsByThreadGroup() {
        ThreadGroup tg = new ThreadGroup("线程组1");
        // 新建 10 个子线程并添加到 tg 线程组中
        for (int i = 0; i < 10; i++) {
            new Thread(tg, "子线程" + (i+1)) {
                @Override
                public void run() {
                    // 当前线程的中断标志为 false 的时候,继续循环
                    while (!currentThread().isInterrupted()) {
                        System.out.println(currentThread().getName() + "打印");
                    }
                }
            }.start();
        }
        try {
            // 主线程休眠 3 秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        }
        // 设置线程中断标志为 true,以中断线程组中的线程
        tg.interrupt();
    }

    public static void startTest() {
        stopThreadsByThreadGroup();
    }
}

public static void main(String[] args) {
    ThreadGroupTest.startTest();
}

我们在代码中利用了线程的中断标志,关于这个,不熟悉的小伙伴可以参考一下本系列的第二篇文章:Java 多线程(2)— 线程的控制:

https://blog.csdn.net/Hacker_ZhiDian/article/details/79522137

我们来看看结果:

640?wx_fmt=png

如果你运行了该程序,你会发现在 10 个子线程交替打印 3 秒之后程序终止,正是因为主线程在休眠 3 秒后我们通过线程组批量停止了该线程组中的所有线程的运行,之后主线程推出,程序结束。

好了,关于线程组就介绍到这里了,对于一些其他的方法用法,小伙伴们可以自己尝试一下,下面来看看 ThreadLocal 类。


ThreadLocal


好吧。这个类的名字有点奇怪,毕竟翻译成中文是:线程本地,不像 ThreadGroup 从名字就可以大概猜到知道是干什么的。我们还是从官方对这个类的介绍开始吧:

This class provides thread-local variables
These variables differ from their normal counterparts in that each thread that 
    accesses one (via its get or set methodhas its own
    independently initialized copy of the variable.
ThreadLocal instances are typically private static fields in classes that 
    wish to associate state with a thread (e.g., a user ID or Transaction ID).

Each thread holds an implicit reference to its copy of a thread-local variable 
    as long as the thread is alive and the ThreadLocal instance is accessible
;
after a thread goes away, 
    all of its copies of thread-local instances are subject to garbage collection 
    (unless other references to these copies exist).

大概意思是: 

这个类提供线程本地变量。这个变量不同于线程中普通的副本变量,因为每个线程都持有一个属于它自己的变量,并且可以通过 get、set 方法来对其进行访问和修改,并且初始时会独立创建变量的副本保存在每个线程中。ThreadLocal 对象一般是一个线程的私有字段,用于和线程中的某个信息相关联(比如:用户 ID、交易 ID)。 

每个线程都拥有对其保存的线程局部变量副本的隐式引用,只要线程处于活动状态并且其对应的 ThreadLocal 对象字段可用时就可以访问。线程结束后,所有的线程保存的 ThreadLocal 对象将会被垃圾回收器回收,除非还有其他的引用指向这些存在的对象。

从官方说明中,我们可以知道利用这个类我们可以在每一个线程中都保存一个变量,并且不同线程中的这个变量互不冲突,我们还可以通过 ThreadLocal 对象的 get、set 方法来读取、修改这个变量的值。那么具体怎么用呢?我们来看看一个简单的例子:

/**
 * ThreadLocal 线程变量副本保存测试
 */

public static class ThreadLocalTest {
    // 新建一个 ThreadLocal 对象
    static ThreadLocal<Integer> value = new ThreadLocal<Integer>();

    public static void startTest() {
        // 新建 5 个子线程,run 方法中调用新建的 ThreadLocal 对象 value 的 get/set 方法来获取/设置对应值
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                public void run() {
                    // 当当前线程中 value 值不大于 5 时候继续循环
                    while (value.get() <= 5) {
                        System.out.println(Thread.currentThread().getName() + " 的 value 值:" + value.get());
                        // 当前线程的 value 自增一
                        value.set(value.get() + 1);
                    }
                };
            }, "线程 " + (i+1)).start();
        }
    }
}

public static void main(String[] args) {
    ThreadLocalTest.startTest();
}

让我们来运行一下: 

640?wx_fmt=png

纳尼,,,903 行代码报空指针异常,没道理吧,value 是一个新建的 ThreadLocal 对象,调用其 get() 方法怎么会空指针。。。为了弄清楚这个问题,我们还是看一下 ThreadLocal 的 get() 方法是怎么写的:

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */

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

这个方法中先是获取了当前执行代码的线程对象,然后调用了 getMap 方法来获取一个 ThreadLocalMap 对象,如果这个对象为空,那么调用并且返回 setInitialValue 方法的返回值,一步步来,我们先看一下 getMap 方法: 

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */

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

可以看到,这个方法直接返回了线程对象 t 的 threadLocals 字段对象,我们来看看这个字段的定义,在 Thread.java中: 

// ...
/**
 * ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class.
 */

ThreadLocal.ThreadLocalMap threadLocals = null;
// ...

可以看到,这个字段对象默认是 null ,并且在线程的构造方法中也没有主动为其赋值,那么回到 ThreadLocal 类中的 get 方法中,此时获取到的线程 t 中的 map 对象值为 null,那么其就会调用 setInitialValue 方法,我们看看 setInitialValue 方法: 

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */

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

我们可以看到在方法中先调用了 initialValue 方法并赋值给了一个泛型的引用 value,最后在这个方法的结尾会将 value 返回。再看看 initialValue 方法:

protected T initialValue() {
    return null;
}

好吧,它直接返回了 null ,那么也就是说 setInitialValue 方法中的 value 值为 null,到这里我们其实已经知道之前的那个程序为什么会导致 NullPointException 异常了,就是因为默认的 initialValue 方法返回的值是 ,当第一次调用其 get 方法时,就会得到一个 值,于是我们也知道解决方案了: 

创建 ThreadLocal 对象时重写其 initialValue 方法并且返回一个非 null 的默认值作为每个线程中储存的变量的默认值。Ok,方案找到了,我们还是多看一步,setInitialValue 方法接下来就会调用ThreadLocal 对象的 createMap 方法,我们再看看这个方法: 

/**
 * Create the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param t the current thread
 * @param firstValue value for the initial entry of the map
 */

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

Ok,到这里,才主动的给线程 t 对象的 threadLocals 字段引用赋了一个新的 ThreadLocalMap 对象。通过名字我们大概能猜到这是一个类似于映射表的对象,并且这个对象的键是当前的 ThreadLocal 对象(传入了 this 参数)。我们来看看这个 ThreadLocalMap 类: 

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

    /**
     * The initial capacity -- MUST be a power of two.
     */

    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */

    private Entry[] table;

    /**
     * The number of entries in the table.
     */

    private int size = 0;
    // ...

这个类时 ThreadLocal 类的静态子类,而且也确实是一个类似于 HashMap 的类,通过一个内部类 Entry 来保存对应的键值对信息,并且键必须是一个 ThreadLocal 对象。回到 ThreadLocal 类的 get 方法,当获取到的当前线程的 ThreadLocalMap 对象不为空的时候,get 方法就会去获取这个 ThreadLocalMap 对象的 Entry 对象,并且这个 Entry 对象的 value 属性值。 

OK,ThreadLocal 类的 get 方法就分析到这里,相信你你已经知道了 get 方法的执行流程,并且也知道了利用 ThreadLocal 类来使得每个线程对象可以存在一个私有 ThreadLocalMap 对象字段,并通过键值对的形式来保存其对应的私有变量副本。这个过程可以用一张图来表示: 

640?wx_fmt=png

也就是说多个线程中的 ThreadLocalMap 字段对象将对应的同一个 ThreadLocal 对象作为 ThreadLocalMap 字段对象的键,而对应储存的值却互相独立。即同一个 ThreadLocal 对象作为多个线程中的 ThreadLocalMap 对象中的键。通过这种机制来完成每个线程中储存一个对应变量的值,不同线程之间这个值相互独立。

这种机制的一个典型的应用是在 Android 系统中对应的 Handler 、 Looper 组成的消息循环机制,熟悉 Android 的小伙伴们可能知道要在一个线程中创建和使用 Handler 的前提就是这个线程已经有一个对应的 Looper 对象,否则的话你就会在创建的 Handler 的时候就会得到一个异常了(RuntimeException(“Can’t create handler inside thread that has not called Looper.prepare()”);)。那么怎么得到对应线程的 Looper 对象呢?其实就是利用的这里的 ThreadLocal 机制,使得每一个线程都可以访问到其对应的 Looper 对象。而主线程的 Looper 对象其实在 ActivityThread 类中的 public static void main(String[] args) 方法中就已经准备好了。

关于 Android 中 Handler 和 Looper 的具体内容,有兴趣的小伙伴可以参考其他的一些资料。下面来更改一下我们前面的那个错误的程序吧,通过前面的分析我们已经知道:只需要在创建 ThreadLocal 对象时重写其 initialValue 方法并且给每个线程中储存对象提供一个默认的值作为返回值就行了:

/**
 * ThreadLocal 线程变量副本保存测试
 */

public static class ThreadLocalTest {
    // 新建一个 ThreadLocal 对象
    static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
        // 重写 initialValue 方法,提供给每个线程保存的对象一个默认的值
        @Override
        protected Integer initialValue() {
            return 0;
        };
    };

    public static void startTest() {
        // 新建 5 个子线程,
        // run 方法中调用新建的 ThreadLocal 对象 value 的 get/set 方法来获取/设置对应值
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                public void run() {
                    // 当当前线程中 value 值不大于 5 时候继续循环
                    while (value.get() <= 5) {
                        System.out.println(Thread.currentThread().getName() + 
                            " 的 value 值:" + value.get());
                        // 当前线程的 value 自增一
                        value.set(value.get() + 1);
                    }
                };
            }, "线程 " + (i+1)).start();
        }
    }
}

public static void main(String[] args) {
    ThreadLocalTest.startTest();
}

 现在来看看结果: 

640?wx_fmt=png

Ok,运行正确,并且结果也在预料之内,每个线程单独持有一个独立的 value 值。

上面我们源码的角度上过了一遍 ThreadLocal 的 get 方法,接下来来看一下其 set 方法:

/**
  * 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);
 }

知道了 get 方法的流程,set 方法就显得简单了:先是获取了当前线程的 ThreadLocalMap 对象,然后判断其是否为 null ,如果不为 null 就直接设置对应的值,否则的话就调用 createMap 方法为当前线程新建一个 ThreadLocalMap 对象。这里的话我们注意到:set 方法在当前线程对象的 ThreadLocalMap 对象为 null 的时候会为当前线程新建一个 ThreadLocalMap 对象,那么对于我们之前遇到的空指针问题,如果在创建 ThreadLocal 对象时不重写其 initialValue 方法。 而是在对应线程第一次调用 get 方法之前先调用 set 方法设置其对应的值也是可以的,当然这个就显得不那么灵活,对调用方法的顺序做了一定限制,小伙伴们了解就好了。

好了, 到这里我们就把 ThreadLocal 类的用法和原理捋了一遍,网上很多博客说利用 ThreadLocal 在某些情况可以代替线程之间的同步机制解决线程之间的同步问题。这里个人觉得还是得看情况而定,因为 ThreadLocal 采用的机制是每个线程内部都保存了一个特定的值,不同的线程的值互不干扰,我们用前面的卖火车票的例子来看,假设我们现在有 10 张火车票,开设 5 个线程卖火车票。然后我们采用 ThreadLocal 类来实现卖票线程之间的同步问题。初始时候每个线程中都储存了 10 张火车票,那么假设某个线程卖出一张票之后,为了数据的同步和正确性,此时还得去更新其他线程的剩余票数(因为每个线程中都储存了一份票数的数据,并且互相独立),那么这样的话反而得不偿失。倒不如直接采用 Java 中的锁机制。


总结


Ok,本专栏的第一系列:多线程体系暂时就到这里了,说实话多线程方面的知识点从来不简单,但是理解原理后也不难,这个还是得多思考,多实践。对于其他的多线程知识点还是等以后碰到了再叙述。这给出该多线程系列的博客中用到的样例代码

https://download.csdn.net/download/hacker_zhidian/10421844

本专栏的下一个系列是 Java 中的集合体系,那会是一个全新的世界。不过这段时间得准备一些别的事,所以应该会到 6 月份之后再更新。 

最后,如果博客中有什么不正确的地方,还请多多指点。如果这篇文章对您有帮助,请不要吝啬您的赞,欢迎继续关注本专栏。谢谢观看!


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

640.png?

640?wx_fmt=jpeg

猜你喜欢

转载自blog.csdn.net/c10wtiybq1ye3/article/details/80768608