java多线程系列 ---- 第二 篇 深入理解Thread与JVM内存

前一篇写了《java多线程系列 ---- 第一篇认识线程
里面相对基础简单一些,这一篇文章就继续往下深入

线程的构造函数

查看Thread源码,我们可以看到Thread的构造函数

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

我们来一一分析这些构造函数

无参构造

我们看一下无参构造的源码

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }


private void init(ThreadGroup g, Runnable target, String name, long stackSize) 

从无参构造中我们可以看到,内部调用了init方法,并且传入默认的参数。
在无参构造中,我们可以看到ThreadGroup为null、Runnable 对象为null,第三个是线程名。最后一个stackSize 设置为0。
现在开始挨个解释这些参数
ThreadGroup g:顾名思义,线程组。ThreadGroup是Java提供的一种对线程进行分组管理的手段,可以对所有线程以组为单位进行操作,如设置优先级、守护线程等。ThreadGroup也是有父子关系的。
在这里设置null,是不是就是没有ThreadGroup呢?no no no,图样图森破。即使是设置null,在init的实现中也是会给当前线程赋值的。那赋什么值呢?默认是使用父线程的ThreadGroup。也就是说,如果不设置,默认是加入父线程所在的组内。
那么ThreadGroup有啥用呢?该怎么用呢?之后再去做详细介绍

Runnable target :这个没啥好介绍的,在之前的文章中对它有介绍。Thread会根据是否传入Runnable对象判断执行自己的run方法还是Runnable对象的run方法。

String name:线程的名字,线程的名字是字符串“Thread-”拼接上当前线程的一个自增数字。这样就能解释为啥我们没有给线程起名字,打印线程名的时候是 Thread-0、Thread-1这样的名字。

long stackSize:堆栈大小,关乎线程的性能。这里设置0会有影响么?设置为0表示不起任何作用。详细内容在下面单独去说

线程名字

线程可以使用一个名字进行构造,但其实除了名字是赋值之外,别的跟无参没什么区别。

   public Thread(String name) {
        init(null, null, name, 0);
    }

建议使用线程的时候给线程起一个合适的名字,当我们对线程监控和管理的时候也会比较方便。

线程改名

当线程创建后,我们还有对线程改名的机会。下面是Thread 的setName方法。从这个方法源码中我们可以看到,如果设置null的话会报NullPointerException异常。如果threadStatus 不等于0 ,就是线程已经启动,会调用 setNativeName(); setNativeName是JNI方法,由底层实现。

扫描二维码关注公众号,回复: 9079320 查看本文章
public final synchronized void setName(String name) {
        checkAccess();
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
        if (threadStatus != 0) {
            setNativeName(name);
        }
    }

之前有教程说线程改名字只能在调用start之前,但经过实际测试,线程start之后也可以改名字。这里我没有研究是java哪个版本可以哪个 版本不可以。我使用的是java 1.8测试。不过嘛如果线程处于死亡状态,改名也是改不成的,线程生命周期都走完了,还改啥名。不过线程死亡调用setName并不会抛出异常,除非是Thread对象已经销毁了,报空指针异常。

Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
                try {
                    TimeUnit.SECONDS.sleep(6);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.setName("123");
        t.start();
        TimeUnit.SECONDS.sleep(2);
        t.setName("456");

输出
在这里插入图片描述

其他构造方法

这里也没有什么必要再去说明了,上面已经阐述了。构造Thread 最后都是调用的init方法,使用构造方法带参传入就是了

stackSize

这里单独说明一下stackSize这个参数,它比较特殊。平时我们也用不到这个参数,但是这个参数对于线程调优还是有一些影响。
一般情况下,创建线程的时候不会手动设置栈内存的地址空间字节数,统一通过xss参数进行设置即可。stacksize越大代表正在线程内方法调用递归的深度越深,stacksize越小代表着创建线程数量越多。这个参数对平台依赖性比较高,如不同的操作系统、不同的硬件表现都不一样。在某些平台上该参数压根不起作用。
stacksize与线程数量的关系也不难理解

进程内存 = 堆内存 + 线程数量 * 栈内存

这里进程的内存是固定的,堆内存作为基数,起到的作用有限。栈内存就是我们设置的stackSize。栈内存越大,线程数量越少,反过来栈内存越小,线程数量就越多。它们是一个反比的关系。

线程与JVM

说道stacksize对于内存的影响,就免不了说一下JVM内存结构。JVM是 Java Virtual Machine的缩写,翻译过来就是java虚拟机。

JVM内存结构

jvm在执行java程序时会把对应的物理内存划分成不同的内存区域,每一个区域都存放着不同的数据,也有不同的创建与销毁的时机。jvm内存结构如图
JVM内存结构图

1 程序计数器

任何编程语言,最终都需要操作系统通过控制总线向CPU发送机器指令,java也是。程序计数器在jvm中的作用就是用于存放当前线程接下来要执行的字节码指令、分支、循环、跳转、异常处理等信息。在任何时候,一个处理器只能执行其中一个线程的指令。为了使CPU调度切换线程能够找到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程间互不影响。jvm也将此块内存区域设计成线程私有

2 java虚拟机栈

这个是重点介绍的内存,与线程紧密关联。java虚拟机栈也是线程私有的,它的生命周期与线程周期相同,在jvm运行时创建。方法在执行的时候会创建一个名为栈帧(stack frame)的数据结构,主要存储局部变量、操作栈、动态链接、方法出口等信息。方法的调用对应着栈帧在虚拟机栈中的压栈和弹栈的过程。
在这里插入图片描述
每个线程在创建的时候,jvm都会创建对应的虚拟机栈,虚拟机栈的大小可以通过 -xss 设置。方法的调用就是栈帧被压入和弹出的过程。一个栈帧占用的内存越小,被压入的栈帧就会越多。一般将栈帧内存大小称为宽度,栈帧数量称为虚拟机栈的深度。我们可以通过优化局部变量等栈帧的组成部分来减少栈帧的内存。

3 本地方法栈

java提供了一些调用本地方法的接口(native interface),也就是native的一些方法,一般是由C/C++实现。我们也会经常用到一些底层的方法,如网络,文件存储扥。JVM为本地方法划分的内存区域辨识本地方法栈。这块内存自由度相当高,由不同的JVM厂商实现。java虚拟机规范没有强制规定,这一部分也是私有的

4 堆内存

堆内存是JVM中最大的一块内存区域,被所有线程共享。在运行中创建的对象几乎都是存放在该内存区。注意是对象!对象!这块内存也是垃圾回收重点区域,堆内存也会被称为“GC堆”。
堆内存一般会分为新生代和老年代,更细致的划分为 Eden区、From Survivor区和To Survivor区
在这里插入图片描述

5 方法区

方法区也是被多个线程共享的额内存区域,它主要用于存储已经被虚拟机加载的类信息,常量、静态变量、即时编译器(JIT Just In Time)编译后的代码等内容。虽然在java虚拟机规范中,将这块内存划分为堆内存的一个逻辑分区,但是它还是经常被称为“非堆”,也会被称为“持久代”。在HotSpot JVM中,方法区还会被细分为持久代和代码缓存区,代码缓存区用于存储编译后的本地代码以及JIT编译器生成的代码。

6 java 8 元空间

在jdk1.8之后,内存区域发生了一点变化。持久代内存被删除,取而代之的是元空间。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

守护线程

什么是守护线程

守护线程是一类特殊线程,一般用于处理 一些后台的工作。正常情况下,JVM程序会在没有任何一条非守护线程运行时退出。换句话说就是,当main线程没有开启线程,或者main线程开启的线程都运行完毕,main线程也运行完毕,则会退出。这里main线程开启的线程不能是守护线程。
也就是说,守护线程是辅助非守护线程的线程,线程结束守护线程也自动结束。

守护线程的使用

守护线程使用非常简单,创建了Thread对象后,调用setDaemon设置为true就可以。

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

        //main线程开始
        Thread thread = new Thread(()->{
           while (true){
               try {
                   TimeUnit.SECONDS.sleep(1);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });

        thread.setDaemon(true);//设置守护线程
        thread.start();//thread 线程开始
        System.out.println("end");//main线程结束
    }

我们也可以通过这个例子看一下守护线程的特点。运行上面代码后,输出结果为:
在这里插入图片描述
这里直接退出了程序。
那如果我们不设置守护线程,在运行一下看情况

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

        //main线程开始
        Thread thread = new Thread(()->{
           while (true){
               try {
                   TimeUnit.SECONDS.sleep(1);
                   System.out.println("子线程运行");
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });

//        thread.setDaemon(true);//设置守护线程
        thread.start();//thread 线程开始
        System.out.println("end");//main线程结束
    }

结果:
在这里插入图片描述
我们可以看到如果非守护线程的话,那么子线程会一直运行,整个程序都不会退出。这样也就看出来了守护线程的特点。
如果不使用setDaemon 设置守线程的话,线程是否为守护线程与父线程也有很大的关系。如果父线程是正常线程,那么子线程也是正常线程。如果父线程是守护线程,那么子线程也是守护线程。我们可以用 isDaemon方法判断是否为守护线程。

守护线程的作用

守护线程的特点是不会影响到主线程的退出。守护线程会自动结束生命周期。垃圾回收就是守护线程,当程序运行的时候,垃圾回收线程正常工作。那如果JVM推出了,垃圾回收线程也不需要去运行了。如果垃圾回收是正常线程的话,那么就会导致JVM无法退出,因为还有线程在一直运行。
守护线程常做用于执行一些后台任务,当希望关闭某些线程或者JVM退出时,可以自动关闭,此时就可以考虑使用守护线程。

发布了95 篇原创文章 · 获赞 36 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/u013513053/article/details/99678411