深入理解Thread构造函数
本文参考汪文君著:java高并发编程详解。
1、线程的命名
在构造现成的时候可以为线程起一个名字。但是我们如果不给线程起名字,那线程会有一个怎样的命名呢?
这里我们看一下thread的源代码:
public thread(threadgroup group, runnable target) { init(group, target, "thread-" + nextthreadnum(), 0); } /** * allocates a new {@code thread} object. this constructor has the same * effect as {@linkplain #thread(threadgroup,runnable,string) thread} * {@code (null, null, name)}. * * @param name * the name of the new thread */ public thread(string name) { init(null, null, name, 0); } /** * allocates a new {@code thread} object. this constructor has the same * effect as {@linkplain #thread(threadgroup,runnable,string) thread} * {@code (group, null, name)}. * * @param group * the thread group. if {@code null} and there is a security * manager, the group is determined by {@linkplain * securitymanager#getthreadgroup securitymanager.getthreadgroup()}. * if there is not a security manager or {@code * securitymanager.getthreadgroup()} returns {@code null}, the group * is set to the current thread's thread group. * * @param name * the name of the new thread * * @throws securityexception * if the current thread cannot create a thread in the specified * thread group */ public thread(threadgroup group, string name) { init(group, null, name, 0); }
如果没有为线程起名字,那么线程将会以“thread-”作为前缀与一个自增数字进行组合,这个自增数字在整个jvm进程中将会不断自增:
如果我们执行以下代码:
import java.util.stream.intstream; public class test { public static void main(string[] args) { intstream.range(0,5).boxed() .map( i->new thread( ()->system.out.println( thread.currentthread().getname() ) ) ).foreach(thread::start); } }
这里使用无参的构造函数创建了5个线程,并且分别输出了各自的名字:
其实thread同样提供了这样的构造函数。如下
thread(runnable target,string name);
thread(string name);
thread(threadgroup group,runnable target,string name);
thread(threadgroup group,runnable target,string name,long stacksize);
thread(threadgroup group,string name);
下面是实现代码:
import java.util.stream.intstream; public class test2 { private final static string prefix="alex-"; public static void main(string[] args) { intstream.range(0,5).maptoobj(test2::createthread).foreach(thread::start); } private static thread createthread(final int intname) { return new thread(()->system.out.println(thread.currentthread().getname()),prefix+intname); } }
运行效果:
需要注意的是,不论你使用的是默认的命名还是特殊的名字,在线程启动之后还有一个机会可以对其进行修改,一旦线程启动,名字将不再被修改,下面是setname源码:
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); } }
2、线程的父子关系
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(); 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(); } } /* checkaccess regardless of whether or not threadgroup is explicitly passed in. */ g.checkaccess(); /* * do we have the required permissions? */ if (security != null) { if (isccloverridden(getclass())) { security.checkpermission(subclass_implementation_permission); } } g.addunstarted(); this.group = g; this.daemon = parent.isdaemon(); this.priority = parent.getpriority(); if (security == null || isccloverridden(parent.getclass())) this.contextclassloader = parent.getcontextclassloader(); else this.contextclassloader = parent.contextclassloader; this.inheritedaccesscontrolcontext = acc != null ? acc : accesscontroller.getcontext(); this.target = target; setpriority(priority); if (inheritthreadlocals && parent.inheritablethreadlocals != null) this.inheritablethreadlocals = threadlocal.createinheritedmap(parent.inheritablethreadlocals); /* stash the specified stack size in case the vm cares */ this.stacksize = stacksize; /* set thread id */ tid = nextthreadid(); }
上面的代码中的currentthread()是获取当前线程,在线程的生命周期中,线程的最初状态为new,没有执行start方法之前,他只能算是一个thread的实例,并不意味着一个新的线程被创建,因此currentthread()代表的将会是创建它的那个线程,因此我们可以得出以下结论:
- 一个线程的创建肯定是由另一个线程完成的
- 被创建线程的父线程是创建它的线程
我们都知道main函数所在的线程是由jvm创建的,也就是main线程,那就意味着我们前面创建的所有线程,其父线程都是main线程。
3、thread与threadgroup
在thread的构造函数中,可以显式地指定线程的group,也就是threadgroup。
在thread的源码中,我们截取片段。
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(); } }
通过对源码的分析,我们不难看出,如果没指定一个线程组,那么子线程将会被加入到父线程所在的线程组,下面写一个简单的代码来测试一下:
package concurrent.chapter02; public class threadconstruction { public static void main(string[] args) { thread t1 = new thread("t1"); threadgroup group = new threadgroup("testgroup"); thread t2 = new thread(group,"t2"); threadgroup mainthreadgroup = thread.currentthread().getthreadgroup(); system.out.println("main thread belong group:"+mainthreadgroup.getname()); system.out.println("t1 and main belong the same group:"+(mainthreadgroup==t1.getthreadgroup())); system.out.println("t2 thread group not belong main group:"+(mainthreadgroup==t2.getthreadgroup())); system.out.println("t2 thread group belong main testgroup:"+(group==t2.getthreadgroup())); } }
运行结果如下所示:
通过上面的例子,我们不难分析出以下结论:
main 线程所在的threadgroup称为main
构造一个线程的时候如果没有显示地指定threadgroup,那么它将会和父线程拥有同样的优先级,同样的daemon。
在这里补充一下thread和runnable的关系。
thread负责线程本身的职责和控制,而runnable负责逻辑执行单元的部分。
4、thread与jvm虚拟机栈
stacksize
在thread的构造函数中,可发现有一个特殊的参数,stacksize,这个参数的作用是什么呢?
一般情况下,创建线程的时候不会手动指定栈内存的地址空间字节数组,统一通过xss参数进行设置即可,一般来说stacksize越大,代表正在线程内方法调用递归的深度就越深,stacksize越小代表着创建的线程数量越多,当然这个参数对平台的依赖性比较高,比如不同的操作系统,不同的硬件。
在有些平台下,越高的stack设定,可以允许的递归深度就越多;反之,越少的stack设定,递归深度越浅。
jvm内存结构
虽然stacksize在构造时无需手动指定,但是我们会发现线程和栈内存的关系非常密切,想要了解他们之间到底有什么必然联系,就需要了解jvm的内存分布机制。
jvm在执行java程序的时候会把对应的物理内存划分成不同的内存区域,每一个区域都存放着不同的数据,也有不同的创建与销毁时机,有些分区会在jvm启动的时候就创建,有些则是在运行时才会创建,比如虚拟机栈,根据虚拟机规范,jvm内存结构如图所示。
1、程序计数器
无论任何语言,其实最终都说需要由操作系统通过控制总线向cpu发送机器指令,java也不例外,程序计数器在jvm中所起的作用就是用于存放当前线程接下来将要执行的字节码指令、分支、循环、跳转、异常处理等信息。在任何时候,一个处理器只执行其中一个线程的指令,为了能够在cpu时间片轮转切换上下文之后顺利回到正确的执行位置,每条线程都需要具有一个独立的程序计数器,各个线程互不影响,因此jvm将此块内存区域设计成了线程私有的。
2、java虚拟机栈
这里需要重点介绍内存,因为与线程紧密关联,与程序计数器内存相类似,java虚拟机栈也是线程私有的,他的生命周期与线程相同,是在jvm运行时所创建的,在线程中,方法在执行的时候都会创建一个名为stack frame的数据结构,主要用于存放局部变量表、操作栈、动态链接,方法出口等信息。
每一个线程在创建的时候,jvm都会认为其创建对应的虚拟机栈,虚拟机栈的大小可以通过-xss来配置,方法的调用是栈帧被压入和弹出的过程,同等的虚拟机栈如果局部变量表等占用内存越小,则可被压入的栈帧就会越多,反之则可被压入的栈帧就会越少,一般将栈帧内存的大小成为宽度,而栈帧的数量称为虚拟机栈的深度。
3、本地方法栈
java中提供了调用本地方法的接口(java native interface),也就是可执行程序,在线程的执行过程中,经常会碰到调用jni方法,jvm为本地方法所划分的内存区域便是本地方法栈,这块内存区域其*度非常高,完全靠不同的jvm厂商来实现,java虚拟机规范并未给出强制的规定,同样他也是线程私有的内存区域。
4、堆内存
堆内存是jvm中最大的一块内存区域,被所有线程所共享,java在运行期间创造的所有对象几乎都放在该内存区域,该内存区域也是垃圾回收器重点照顾的区域,因此有时候堆内存被称为“gc堆”。堆内存一般会被细分为新生代和老年代,更细致的划分为eden区,fromsurvivor区和to survivor区。
5、方法区
方法区也是被多个线程所共享的内存区域,它主要用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虽然在java虚拟机规范中,将堆内存划分为对内存的一个逻辑分区,但是它还是经常被称作“非堆”,有时候也被称为“持久代”,主要是站在垃圾回收器的角度进行划分,但是这种叫法比较欠妥,在hotspot jvm中,方法区还会被细划分为持久代和代码缓存区,代码缓存区主要用于存储编译后的本地代码(和硬件相关)以及jit 编译器生成的代码,当然不同的jvm会有不同的实现。
6、java 8 元空间
上述内容大致的介绍了jvm的内存划分,在jdk1.8版本以前的内存大致都是这样划分的,但是从jdk1.8来,jvm的内存区域发生了一些改变,实际上是持久代内存被彻底删除,取而代之的是元空间。
综上,虚拟机栈内存是线程私有的,也就是说每一个线程都会占有指定的内存大小,我们粗略的认为一个java进程的内存大小为:堆内存+线程数量*栈内存。
不管是32位操作系统还是64位操作系统,一个进程最大内存是有限制的。简单来说 线程的数量和虚拟机栈的大小成反比。
5、守护线程
守护线程是一类比较特殊的线程,一般用于处理一些后台的工作,比如jdk的垃圾回收线程。
jvm在什么情况下会退出。
在正常情况下,jvm中若没有一个非守护线程,则jvm的进程会退出。
这和操作系统的线程概念如出一辙。
什么是守护线程?我们看下下面的代码:
public class daemonthread { public static void main(string[] args) throws interruptedexception { thread thread = new thread(()-> { while(true) { try { thread.sleep(1); }catch(exception e) { e.printstacktrace(); } } }); thread.start(); thread.sleep(2_000l); system.out.println("main thread finished lifestyle"); } }
执行这段代码之后,我们会发现,jvm永远不会结束。
package concurrent.chapter02; public class daemonthread { public static void main(string[] args) throws interruptedexception { thread thread = new thread(()-> { while(true) { try { thread.sleep(1); }catch(exception e) { e.printstacktrace(); } } }); thread.setdaemon(true); thread.start(); thread.sleep(2_000l); system.out.println("main thread finished lifestyle"); } }
我们加了个thread.setdaemon(true)之后,程序就在main结束后正常推出了。
注意:
设置守护线程的方法很简单,调用setdaemon方法即可,true代表守护线程,false代表正常线程。
线程是否为守护线程和他的父线程有很大的关系,如果父线程是正常的线程,则子线程也是正常线程,反之亦然,如果你想要修改他的特性则可借助setdaemon方法。isdaemon方法可以判断该线程是不是守护线程。
另外要注意的是,setdaemon方法旨在线程启动之前才能生效,如果一个线程已经死亡,那么再设置setdaemon就会抛出illegalthreadstateexception异常。
守护线程的作用:
在了解了什么是守护线程以及如何创建守护线程之后,我们来讨论一下为什么要有守护线程,以及何时使用守护线程。
通过上面的分析,如果一个jvm进程中没有一个非守护线程,那么jvm就会退出,就是说守护线程具备自动结束生命周期的特性,而非守护线程则不具备这个特点,试想一下弱国jvm进程的垃圾回收线程是非守护线程,如果main线程完成了工作,则jvm无法退出,因为垃圾回收线程还在正常的工作。再比如有一个简单的游戏程序,其中有一个线程正在与服务器不断地交互以获得玩家最新的金币,武器信息,若希望在退出游戏客户端的时候,这些数据的同步工作也能够立即结束等等。
守护线程经常用作与执行一些后台任务,因此有时称他为后台线程,当你希望关闭某些线程的时候,这些数据同步的工作也能够立即结束,等等。
守护线程经常用作执行一些后台任务,因此有时它也被称为后台线程,当你希望关闭这些线程的时候,或者退出jvm进程的时候,一些线程能够自动关闭,此时就可以考虑用守护线程为你完成这样的工作。
总结:
学习了thread的构造函数,能够理解线程与jvm内存模型的关系,还明白了什么是守护线程。