专栏首页指点的专栏Java 多线程(8)---- 线程组和 ThreadLocal

Java 多线程(8)---- 线程组和 ThreadLocal

前言

在上面文章中,我们从源码的角度上解析了一下线程池,并且从其 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();
}

来看看结果:

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

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

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

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

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

// 创建一个指定名称(name)的新线程组,以 parent 线程组作为父线程组
ThreadGroup​(ThreadGroup parent, String name)

// 返回在当前线程组和子线程组中活动的线程的估计数量(注意是估计数量)
int activeCount​()

// 返回在当前线程组和子线程组中活动的线程组的估计数量(注意是估计数量)
int activeGroupCount​() 

// 判断当前执行这个方法的线程有没有权限更改当前线程组的属性,
// 如果没有,那么抛出一个 SecurityException 异常
void    checkAccess​()  

// 清除当前线程组和其子线程组,需要保证当前线程组和其子线程组中的所有线程都已经停止了
void    destroy​()  

// 将当前线程组和其子线程组中的线程拷贝到参数指定的线程数组中,
// 如果线程数组的长度小于线程组中线程的数量,那么多余的线程不会拷贝
int enumerate​(Thread[] list)   

// 将当前线程组中的线程拷贝到参数指定的线程数组中,如果 recurse 参数为 true,
// 那么会递归将其子线程组中的线程也拷贝,
// 如果线程数组的长度小于线程组中线程的数量,那么多余的线程不会拷贝
int enumerate​(Thread[] list, boolean recurse)  

// 将当前线程组(不包括本身)和其子线程组中的线程组拷贝到参数指定的线程组数组中,
// 如果线程组数组的长度小于线程组的数量,那么多余的线程组不会拷贝
int enumerate​(ThreadGroup[] list)  

// 将当前线程组(不包括本身)中的子线程组拷贝到参数指定的线程组数组中,如果 recurse 参数为 true,
// 那么会递归将其子线程组中的子线程组也拷贝,
// 如果线程数组的长度小于线程组中线程的数量,那么多余的线程不会拷贝
int enumerate​(ThreadGroup[] list, boolean recurse) 

// 获取线程组中最大的线程优先级
int getMaxPriority​()   

// 获取线程组名
String  getName​()  

// 获取线程组的父线程组
ThreadGroup getParent​()    

// 中断线程组中所有的线程(调用线程的 Thread.interrupt() 方法)
void    interrupt​()    

// 判断当前线程组是否为守护线程组。守护线程组会在其中的所有的线程停止后和所有的子线程组销毁之后自动销毁
boolean isDaemon​() 

// 判断线程组是否已经销毁
boolean isDestroyed​()  

// 打印线程组的相关信息到控制台中,用于调试
void list​()    

// 判断当前线程组是否为线程组 g 的父线程组或者是祖先线程组
boolean parentOf​(ThreadGroup g)    

// 将线程组设置为守护线程组或者普通线程组
void setDaemon​(boolean daemon) 

// 设置当前线程组中线程允许的最大优先级,线程组中已经存在的优先级大于这个 pri 参数的线程不会受影响,
// 小于这个 pri 参数的线程和之后添加的线程设置的最大优先级不能超过这个值,
// 如果 pri 参数值小于 Thread.MIN_PRIORITY 或者大于  Thread.MAX_PRIORITY,那么会被忽略,
// 如果当前线程组的父线程组不为 null,那么当前线程组的最大优先级设置为 pri 和父线程组优先级较小的那一个,
// 否则直接设置为 pri 。
void setMaxPriority​(int pri)   

// 不安全的方法,和 Thread.stop 方法一样可能造成未知错误
void stop​()    

// 不安全的方法,容易造成死锁,和 Thread.suspend 方法一样
void suspend​() 

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

Thread.java

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

ThreadGroup.java

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

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

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

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)— 线程的控制 。我们来看看结果:

如果你运行了该程序,你会发现在 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 method) has 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).

大概意思是: 这个类提供线程本地变量。这个变量不同于线程中普通的副本变量,因为每个线程都持有一个属于它自己的变量,并且可以通过 getset 方法来对其进行访问和修改,并且初始时会独立创建变量的副本保存在每个线程中。ThreadLocal 对象一般是一个线程的私有字段,用于和线程中的某个信息相关联(比如:用户 ID、交易 ID)。 每个线程都拥有对其保存的线程局部变量副本的隐式引用,只要线程处于活动状态并且其对应的 ThreadLocal 对象字段可用时就可以访问。线程结束后,所有的线程保存的 ThreadLocal 对象将会被垃圾回收器回收,除非还有其他的引用指向这些存在的对象。

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

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

让我们来运行一下:

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

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

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

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

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

好吧,它直接返回了 null ,那么也就是说 setInitialValue 方法中的 value 值为 null,到这里我们其实已经知道之前的那个程序为什么会导致 NullPointException 异常了,就是因为默认的 initialValue 方法返回的值是 ,当第一次调用其 get 方法时,就会得到一个 值,于是我们也知道解决方案了: 创建 ThreadLocal 对象时重写其 initialValue 方法并且返回一个非 null 的默认值作为每个线程中储存的变量的默认值。Ok,方案找到了,我们还是多看一步,setInitialValue 方法接下来就会调用ThreadLocal 对象的 createMap 方法,我们再看看这个方法:

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

这个类时 ThreadLocal 类的静态子类,而且也确实是一个类似于 HashMap 的类,通过一个内部类 Entry 来保存对应的键值对信息,并且键必须是一个 ThreadLocal 对象。回到 ThreadLocal 类的 get 方法,当获取到的当前线程的 ThreadLocalMap 对象不为空的时候,get 方法就会去获取这个 ThreadLocalMap 对象的 Entry 对象,并且这个 Entry 对象的 value 属性值。 OK,ThreadLocal 类的 get 方法就分析到这里,相信你你已经知道了 get 方法的执行流程,并且也知道了利用 ThreadLocal 类来使得每个线程对象可以存在一个私有 ThreadLocalMap 对象字段,并通过键值对的形式来保存其对应的私有变量副本。这个过程可以用一张图来表示:

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

这种机制的一个典型的应用是在 Android 系统中对应的 HandlerLooper 组成的消息循环机制,熟悉 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 中 HandlerLooper 的具体内容,有兴趣的小伙伴可以参考其他的一些资料。

下面来更改一下我们前面的那个错误的程序吧,通过前面的分析我们已经知道:只需要在创建 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();
}

现在来看看结果:

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

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

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

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

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

这给出该多线程系列的博客中用到的样例代码:ThreadTest.zip

本专栏的下一个系列是 Java 中的集合体系,那会是一个全新的世界。不过这段时间得准备一些别的事,所以应该会到 6 月份之后再更新。 最后,如果博客中有什么不正确的地方,还请多多指点。如果这篇文章对您有帮助,请不要吝啬您的赞,欢迎继续关注本专栏。

谢谢观看。。。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 多线程(4)---- 线程的同步(中)

    在前一篇文章: Java 多线程(3)— 线程的同步(上) 中,我们看了一下 Java 中的内存模型、Java 中的代码对应的字节码(包括如何生成 Java 代...

    指点
  • Java 多线程(7)----线程池(下)

    在上篇文章:Java 多线程—线程池(上) 中我们看了一下 Java 中的阻塞队列,我们知道阻塞队列是一种可以对线程进行阻塞控制的队列,并且在前面我们也使用了阻...

    指点
  • Java 多线程(1)---- 初识线程

    多线程想必大家都不会陌生。因为在日常使用和开发中,多线程的使用实在是太常见了。我们都知道,发明多线程的目的是为了更好的利用计算机的 CPU 资源。比如在一个进程...

    指点
  • Java并发编程73道面试题及答案——稳了

    任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thre...

    好好学java
  • 理解线程池,看这篇足够了。

    海仔
  • java中线程池的几种实现方式

    多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力.

    海仔
  • 如何编写线程安全的代码?

    相信有很多同学在面对多线程代码时都会望而生畏,认为多线程代码就像一头难以驯服的怪兽,你制服不了这头怪兽它就会反过来吞噬你。

    用户1516716
  • 100道Java并发和多线程基础面试题大集合(含解答),这波面试稳了~

    这些多线程的问题来源于各大网站,可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都看过,但是本文写作的重心就是所有的问题都会按照自己的理解回...

    程序员白楠楠
  • 线程的创建

    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。

    黑洞代码
  • 面试必考——线程池原理概述

    线程池的源码解析较为繁琐。各位同学必须先大体上理解线程池的核心原理后,方可进入线程池的源码分析过程。

    黑洞代码

扫码关注云+社区

领取腾讯云代金券