Java 中提供了丰富的构造函数,在本文将介绍每一个构造函数,以及分析一些可能你未关注的细节
线程的命名
在构造线程的时候可以为线程起一个特殊意义的名字,有利于实际问题的排查和线程跟踪
线程的默认命名
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
如果没有给线程显示命名,那么线程将以“Thread-”为前缀与一个自增的数字进行组合
命名线程
Thread提供了如下的关于命名的构造函数,通过构造一个友好的名称是一个很好的实战方式
Thread(String name)
Thread(Runnable target, String name)
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
修改线程名字
无论是使用默认的命名规则,还是指定一个特殊的名字,在线程启动之前都有一次机会进行修改,一旦线程启动之后,名字就不能修改了
public final synchronized void setName(String name) {
checkAccess();
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
if (threadStatus != 0) { // 当线程状态不是New的时候,不允许修改线程名称
setNativeName(name);
}
}
线程的父子关系
通过源码,我们发现Thread 的所有构造函数,最终都会去调用一个静态方法init
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread(); // 获取当前线程作为父线程
SecurityManager security = System.getSecurityManager();
.......
}
其中currentThrea() 是获取当前线程,我们知道在线程生命周期中,线程最初的状态为New,没有执行start方法,它只能算一个Thread实例,这里currentThrea() 代表的是创建它的那个线程,因此我们可以得出
- 一个线程的创建肯定是由另外一个线程完成的
- 被创建的父线程是创建它的线程
我们知道main函数所在的线程是JVM创建,也就是main线程,意味我们之前创建所有线程的父线程都是main线程
Thread 与 ThreadGroup
通过继续阅读源码发现在Thread的创建中,我们可以显示的指定线程的Group
SecurityManager security = System.getSecurityManager();
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) {
g = parent.getThreadGroup();
}
}
当构造Thread没有显示指定ThreadGroup时候,子线程将会加入父线程所在的线程组
- main线程所在的ThreadGroup 称为main
- 构造一个线程的时候如果没有显示指定ThreadGroup,那它将会和父线程同属于一个ThreadGroup
默认设置中,子线程会和父线程除了同属于一个Group之外,它还会和父线程拥有同样的优先级,同样的daemon
Thread 与 Runnable
Thread负责线程相关的职责和控制,而Runnable负责逻辑执行单元部分
Thread 与 虚拟机栈
在Thread的构造函数中,我们发现一个特殊的参数stackSize,这个参数的作用是什么?
一般情况下,创建线程的时候不需要手动指定栈内存的地址空间字节数组,但通过stacksize的设置可以发现,stackSize越大,线程内方法调用递归的深度就越深,stacksize越小创建的线程数量越多,当然这个参数高度依赖平台
JVM内存结构
JVM在执行Java程序的时候会把对应的物理内存划分不同的区域,每个区域都存放不同的数据,也有不同的创建和销毁时机,有些分区在JVM启动的时候就创建,有些则在运行才创建,java1.8之前,JVM内存结构大致如下图所示:
程序计数器
程序计数器在JVM中作用就是存放当前线程接下来要执行的字节码指令,分支,循环,跳转,异常处理信息,为了在CPU时间片轮换上下文之后顺利回到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程互不影响,因此JVM将此块区域设计成线程私有
java虚拟机栈
虚拟机栈是与线程紧密关联的一块区域,与程序计数器相似,Java虚拟机栈也是线程私有的,它的生命周期与线程相同,是在JVM运行时所创建的,在线程中,方法在执行的时候会创建一个名为栈帧的数据结构,主要存放局部变量表,操作栈,动态链接,方法出口等信息,如下图,方法的调用对应着栈帧在虚拟机中压栈和出栈的过程
每一个线程在创建的时候,JVM都会为其创建对应的虚拟机栈,虚拟机栈的大小可以通过-xss来配置,方法的调用是栈帧被压入和弹出的操作,同等的虚拟机栈如果局部变量表占用内存越少,则被压入栈帧数量就会越多
本地方法栈
Java中提供调用本地方法的接口,也就是C/C++程序,在线程执行过程中,经常会碰到调用JNI方法的情况,比如网络通信,文件操作等底层,JVM为本地方法划分的内存区域就是本地方法栈,这块内存区域自由度很高,完全依靠JVM厂商实现,同样也是线程私有的内存区域
堆内存
堆内存是JVM中最大一块内存区域,被所有的线程共享,Java运行期间创建的所有对象几乎存在此块区域里,该内存区域也是回收器重点照顾的区域,因此也称为GC堆,堆内存一般会细分为新生代和老年代,新生代更细致划分为Eden区,From Survivor区和To Survivor区
方法区
方法区也是被多个线程共享的区域,主要存储以及被虚拟机加载的类信息,常量,静态变量,即时编译器(JIT) 编译后的代码等数据,虽然在Java虚拟机规范中,将方法区划分为堆内存中的一个逻辑分区,但它还是经常被称为“非堆”,有时候也称为“持久代”
Java8 元空间
自java1.8之后,JVM内存发生了一些改变,实际上持久代内存被彻底删除,这部分区域被Meta space代替,不同的是元空间使用的本地内存
为什么移除持久代
- 它的大小是在启动时固定好的——很难进行调优。-XX:MaxPermSize,设置成多少好呢?
- HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型的,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)
- 可以在GC不进行暂停的情况下并发地释放类数据
- 使得原来受限于持久代的一些改进未来有可能实现
守护线程
守护线程是一类比较特殊的线程,一般用来处理一些后台的工作,比如JDK垃圾回收线程
在正常情况下,若JVM中没有一个非守护线程,则JVM进行会退出,需要注意setDaemon方法必须在线程启动之前设置才有效
守护线程具有自动结束生命周期的特性,而非守护线程不具备,比如在一个简单游戏中,其中一个线程能不断与服务器交互获取玩家最新金币和武器信息,若希望在退出客户端时候,这个线程也能立即结束,我们往往可以将这个线程设置为守护线程
守护线程一般执行一些后台任务,如果你希望关闭某些线程的时候,或者退出JVM进程时候,这些线程能自动关闭,可以考虑用守护线程来完成这样的工作