上一篇快速认识线程
本文参考汪文君著: 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 内存模型的关系, 还明白了什么是守护线程.
来源: https://www.cnblogs.com/godoforange/p/11016706.html