深入理解Thread构造函数

1 线程的命名

在构造线程的时候可以为线程起一个有特殊意义的名字,这也是比较好的一种做法,尤其在一个线程比较多的程序中,为线程赋予一个包含特殊意义的名字有助于问题的排查和线程的跟踪,推荐在构造线程的时候赋予它一个名字。

1.1 默认线程命名规则

下面的几个构造函数中,并没有提供为线程命名的参数,那么此时线程会有一个怎样的命名呢?

  • Thread ()
  • Thread(Runnable target)
  • Thread(ThreadGroup group,Runnable target)
public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
}
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}
  • 如果没有为线程显式地指定一个名字,那么线程将会以“Thread-”作为前缀与一个自增数字进行组合,这个自增数字在整个JVM进程中将会不断自增:
public static void main(String[] args) {

   for(int i = 0; i <= 4; i++){
       Thread thread = new Thread();
       // 查看一下名字
       System.out.println(thread.getName());
   }
}
Thread-0
Thread-1
Thread-2
Thread-3
Thread-4

1.2 如何自定义命名线程

Thread同样也提供了这样的构造函数,具体如下:

public Thread(Runnable target,String name);
public Thread(String name);
public Thread(ThreadGroup group,Runnable target,String name);
public Thread(ThreadGroup group,Runnable target,String name,long stackSize);
public Thread(ThreadGroup group,String name);

1.3 修改线程的名字

public final synchronized void setName(String name) {
   checkAccess();
   if (name == null) {
       throw new NullPointerException("name cannot be null");
   }
   this.name = name;
   // 线程不是NEW状态,对其的修改将不会生效
   if (threadStatus != 0) {
       setNativeName(name);
   }
}

2 线程的父子关系

Thread的所有构造函数,最终都会去调用一个静态方法init,我们截取片段代码对其进行分析,不难发现新创建的任何一个线程都会有一个父线程:

this.name = name;
// 获取当前线程作为父线程
Thread parent = currentThread();
if (g == null) {
/* Determine if it's an applet or not */

/* If there is a security manager, ask the security manager
   what to do. */
if (security != null) {
    g = security.getThreadGroup();
}

/* If the security doesn't have a strong opinion of the matter
   use the parent thread group. */
if (g == null) {
	// 如果这个线程的线程组是null,获取parent所属的线程组
    g = parent.getThreadGroup();
}

那么现在这个parent线程是是哪个线程呢?
此时在new Thread对象,所以当前currentThread()代表的将会是创建它的那个线程。

测试一下:

public class Demo02 {

    public static void main(String[] args) {
        Thread thread = new MyThead();
    }
}
class MyThead extends Thread{

    /**
     * 重写一个父类构造,打印一下currentThread()
     */
    public MyThead() {
        super();
        // 打印一下currentThread 的namne
        System.out.println(currentThread().getName());

    }
}

创建MyThead的这个实例是main线程创建的,所以打印的结果就是main

总结:

  • 一个线程的创建肯定是由另一个线程完成的。
  • 被创建线程的父线程是创建它的线程。

3 线程和线程组

在Thread的构造函数中,可以显式地指定线程的Group,也就是ThreadGroup。根据上面的代码:

if (g == null) {
	// 如果没有明确指定线程组,就会采用父线程(创建当前线程的线程)的线程组
	// 换句话说,就是会和父线程同属一个组
    g = parent.getThreadGroup();
}

这里就不测试了 :

  • main线程所在的ThreadGroup称为main。
  • 构造一个线程的时候如果没有显式地指定ThreadGroup,那么它将会和父线程同属于一个ThreadGroup。

另外:在默认设置中,当然除了子线程会和父线程同属于一个Group之外,它还会和父线程拥有同样的优先级,同样的daemon

4 Thread与JVM虚拟机栈

Thread的构造函数中,可发现有一个特殊的参数stackSize,这个参数的作用是什么呢?它的值对线程有什么影响呢?下面我们就来一起探讨这个问题。

jdk文档:

The stack size is the approximate number of bytes of address space that the virtual machine is to allocate for this thread’s stack.The effect of the stackSize parameter,if any,is highly platform dependent. (堆栈大小是虚拟机为这个线程的堆栈分配的地址空间的大约字节数。stackSize参数的影响,如果有,是高度依赖于平台的。)

On some platforms,specifying a higher value for the stackSize parameter may allow a thread to achieve greater recursion depth before throwing a StackOverflowError.Similarly,specifying a lower value may allow a greater number of threads to exist concurrently without
throwing an OutOfMemoryError(or other internal error).The details of the relationship between the value of the stackSize parameter > and the maximum recursion depth and concurrency level are platform-dependent.On some platforms,the value of the stackSize > parameter may have no effect whatsoever.

  • 一般情况下,创建线程的时候不会手动指定栈内存的地址空间字节数组,统一通过xss参数进行设置即可。
  • 通过上面这段官网文档的描述,我们不难发现stacksize越大则代表着正在线程内方法调用递归的深度就越深,stacksize越小则代表着创建的线程数量越多,当然了这个参数对平台的依赖性比较高,比如不同的操作系统、不同的硬件。
  • 在有些平台下,越高的stack设定,可以允许的递归深度越多;反之,越少的stack设定,则递归深度越浅。当然在某些平台下,该参数压根不会起到任何作用,如果将该参数设置为0,也不会起到任何的作用
  • 该参数一般情况下不会主动设置,采用系统默认的值就可以了,默认情况下会设置成0。

4.1 JVM内存结构简单介绍

JVM在执行Java程序的时候会把对应的物理内存划分成不同的内存区域,每一个区域都存放着不同的数据,也有不同的创建与销毁时机,有些分区会在JVM启动的时候就创建,有些则是在运行时才创建,比如虚拟机栈,根据虚拟机规范,JVM的内存结构如图2-3所示:

在这里插入图片描述

  1. 程序计数器:
    无论任何语言,其实最终都是需要由操作系统通过控制总线向CPU发送机器指令,Java也不例外,程序计数器在JVM中所起的作用就是用于存放当前线程接下来将要执行的字节码指令、分支、循环、跳转、异常处理等信息。在任何时候,一个处理器只执行其中一个线程中的指令,为了能够在CPU时间片轮转切换上下文之后顺利回到正确的执行位置,每条线程都需要具有一个独立的程序计数器,各个线程之间互相不影响,因此JVM将此块内存区域设计成了线程私有的。

  2. Java虚拟机栈
    与程序计数器内存相类似,Java虚拟机栈也是线程私有的,它的生命周期与线程相同,是在JVM运行时所创建的,在线程中,方法在执行的时候都会创建一个名为栈帧(stack frame)的数据结构,主要用于存放局部变量表、操作栈、动态链接、方法出口等信息。,方法的调用对应着栈帧在虚拟机栈中的压栈和弹栈过程。
    每一个线程在创建的时候,JVM都会为其创建对应的虚拟机栈,虚拟机栈的大小可以通过-xss来配置,方法的调用是栈帧被压入和弹出的过程,同等的虚拟机栈如果局部变量表等占用内存越小则可被压入的栈帧就会越多,反之则可被压入的栈帧就会越少,一般将栈帧内存的大小称为宽度,而栈帧的数量则称为虚拟机栈的深度。
    在这里插入图片描述

  3. 本地方法栈
    Java中提供了调用本地方法的接口(Java Native Interface),也就是C/C++程序,在线程的执行过程中,经常会碰到调用JNI方法的情况,比如网络通信、文件操作的底层,甚至是String的intern等都是JNI方法,JVM为本地方法所划分的内存区域便是本地方法栈,这块内存区域其自由度非常高,完全靠不同的JVM厂商来实现,Java虚拟机规范并未给出强制的规定,同样它也是线程私有的内存区域。

  4. 堆内存
    堆内存是JVM中最大的一块内存区域,被所有的线程所共享,Java在运行期间创建的所有对象几乎都存放在该内存区域,该内存区域也是垃圾回收器重点照顾的区域,因此有些时候堆内存被称为“GC堆”。

    堆内存一般会被细分为新生代和老年代,更细致的划分为Eden区、From Survivor区和To Survivor区

  5. 方法区
    方法区也是被多个线程所共享的内存区域,他主要用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据,虽然在Java虚拟机规范中,将堆内存划分为堆内存的一个逻辑分区,但是它还是经常被称为“非堆”,有时候也被称为“持久代”,主要是站在垃圾回收器的角度进行划分,但是这种叫法比较欠妥,在HotSpot JVM中,方法区还会被细划分为持久代和代码缓存区,代码缓存区主要用于存储编译后的本地代码(和硬件相关)以及JIT(Just In Time)编译器生成的代码,当然不同的JVM会有不同的实现

  6. Java 8元空间
    在JDK1.8版本以前的内存大概都是这样划分的,但是自JDK1.8版本起,JVM的内存区域发生了一些改变,实际上是持久代内存被彻底删除,取而代之的是元空间。
    而在JDK1.8版本中,该内存区域被Meta Space取而代之了,元空间同样是堆内存的一部分,JVM为每个类加载器分配一块内存块列表,进行线性分配,块的大小取决于类加载器的类型,sun/反射/代理对应的类加载器块会小一些,之前的版本会单独卸载回收某个类,而现在则是GC过程中发现某个类加载器已经具备回收的条件,则会将整个类加载器相关的元空间全部回收,这样就可以减少内存碎片,节省GC扫描和压缩的时间

4.2 Thread与虚拟机栈

JVM的内存分布,其中程序计数器是比较小的一块内存,而且该部分内存是不会出现任何溢出异常的,与线程创建、运行、销毁等关系比较大的是虚拟机栈内存了,而且栈内存划分的大小将直接决定在一个JVM进程中可以创建多少个线程.

  • 线程的创建数量是随着虚拟机栈内存的增多而减少的,也就是一种反比关系。

虚拟机栈内存是线程私有的,也就是说每一个线程都会占有指定的内存大小,我们粗略地认为一个Java进程的内存大小为:堆内存+线程数量*栈内存。不管是32位操作系统还是64位操作系统,一个进程的最大内存是有限制的,比如32位的Windows操作系统所允许的最大进程内存为2GB,因此根据上面的公式很容易得出,线程数量与栈内存的大小是反比关系,那么线程数量与堆内存的大小关系呢?当然也是反比关系,只不过堆内存是基数,而栈内存是系数而已,堆内存作为影响进程内存的基数,它的增大对线程数量的影响也是反比关系,但是并没有像栈内存那样明显

在JVM中到底可以创建多少个线程,与堆内存、栈内存的大小有着直接的关系,只不过栈内存更加明显一些,前文中我们说过在操作系统中一个进程的内存大小是有限制的,这个限制称为地址空间,比如32位的Windows操作系统最大的地址空间约为2G多一点,操作系统则会将进程内存的大小控制在最大地址空间以内,下面的公式是一个相对比较精准的计算线程数量的公式,其中ReservedOsMemory是系统保留内存,一般在136MB左右:

线程数量=(最大地址空间(MaxProcessMemory)-JVM堆内存-ReservedOsMemory)/ThreadStackSize(XSS)

当然线程数量还与操作系统的一些内核配置有很大的关系

5 守护线程

守护线程是一类比较特殊的线程,一般用于处理一些后台的工作,比如JDK的垃圾回收线程。

守护线程是一类比较特殊的线程,一般用于处理一些后台的工作,比如JDK的垃圾回收线程。(在正常情况下,若JVM中没有一个非守护线程,则JVM的进程会退出。)

5.1 什么是守护线程

通过一个小demo,简单认识一下守护线程:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + "正在运行。。。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    },"我的线程");
    // 启动线程
    thread.start();
    // 主线程休息5s后就结束了
    TimeUnit.SECONDS.sleep(5);
    System.out.println("Main thread finished lifecycle.");
}

输出结果:

我的线程正在运行。。。
我的线程正在运行。。。
我的线程正在运行。。。
我的线程正在运行。。。
Main thread finished lifecycle.
我的线程正在运行。。。
我的线程正在运行。。。
我的线程正在运行。。。
我的线程正在运行。。。
我的线程正在运行。。。
我的线程正在运行。。。

可以发现当主线程结束后,Java虚拟机并没有退出,我们自定义的这个线程还在运行。

现在将其设置为守护线程

public static void main(String[] args) throws InterruptedException {
   Thread thread = new Thread(() -> {
       while (true) {
           try {
               TimeUnit.SECONDS.sleep(1);
               System.out.println(Thread.currentThread().getName() + "正在运行。。。");
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
   },"我的线程");
   // 设置为守护线程 
   thread.setDaemon(true);
   // 启动线程
   thread.start();
   // 主线程休息5s后就结束了
   TimeUnit.SECONDS.sleep(5);
   System.out.println("Main thread finished lifecycle.");
}
我的线程正在运行。。。
我的线程正在运行。。。
我的线程正在运行。。。
我的线程正在运行。。。
Main thread finished lifecycle.

发现当主线程结束收,只剩下守护线程的时候,java虚拟机就自动退出了。

总结:

  • 设置守护线程的方法很简单,调用setDaemon方法即可,true代表守护线程,false代表正常线程。
  • 线程是否为守护线程和它的父线程有很大的关系,如果父线程是正常线程,则子线程也是正常线程,反之亦然,如果你想要修改它的特性则可以借助setDaemon方法。
  • isDaemon方法可以判断该线程是不是守护线程。
  • setDaemon方法只在线程启动之前才能生效,如果一个线程已经死亡,那么再设置setDaemon则会抛出IllegalThreadStateException异常。

测试:线程是否为守护线程和它的父线程有很大的关系,如果父线程是正常线程,则子线程也是正常线程,反之亦然

public static void main(String[] args) throws InterruptedException {

    Thread thread = new Thread(() -> {
        // 在这个线程里面在创建一个线程
        Thread son = new Thread("子线程");
        System.out.println("子线程是守护线程吗? " + son.isDaemon());
    },"父线程");
    // 设置父线程为守护线程
    thread.setDaemon(true);
    // 启动线程
    thread.start();
    // 主线程休息5s后就结束了
    TimeUnit.SECONDS.sleep(5);
    System.out.println("Main thread finished lifecycle.");
}

输出结果:子线程是守护线程吗? true

可见默认线程是否是守护线程取决于创建该线程的线程。

可以查看Thread的init方法的源码里面就有这么一句:

Thread parent = currentThread();
this.daemon = parent.isDaemon();

5.2 守护线程的作用

  • 如果一个JVM进程中没有一个非守护线程,那么JVM会退出,也就是说守护线程具备自动结束生命周期的特性,而非守护线程则不具备这个特点。

试想一下如果JVM进程的垃圾回收线程是非守护线程,如果main线程完成了工作,则JVM无法退出,因为垃圾回收线程还在正常的工作。再比如有一个简单的游戏程序,其中有一个线程正在与服务器不断地交互以获取玩家最新的金币、武器信息,若希望在退出游戏客户端的时候,这些数据同步的工作也能够立即结束.

守护线程经常用作与执行一些后台任务,因此有时它也被称为后台线程,当你希望关闭某些线程的时候,或者退出JVM进程的时候,一些线程能够自动关闭,此时就可以考虑用守护线程为你完成这样的工作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值