一个线程两次调用start()方法会出现什么情况?

news2024/9/22 23:20:56

第17讲 | 一个线程两次调用start()方法会出现什么情况?

在这里插入图片描述

今天我们来深入聊聊线程,相信大家对于线程这个概念都不陌生,它是 Java 并发的基础元素,理解、操纵、诊断线程是 Java 工程师的必修课,但是你真的掌握线程了吗?

今天我要问你的问题是,一个线程两次调用 start() 方法会出现什么情况?谈谈线程的生命周期和状态转移。

典型回答

Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。

关于线程生命周期的不同状态,在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中,分别是:

新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个 Java 内部状态。

就绪(RUNNABLE),表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。

在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java API 的角度,并不能表示出来。

阻塞(BLOCKED),这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待 Monitor lock。比如,线程试图通过 synchronized 去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。

等待(WAITING),表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似 notify 等动作,通知消费线程可以继续工作了。Thread.join() 也会令线程进入等待状态。

计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时版本,如下面示例:

public final native void wait(long timeout) throws InterruptedException;

终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。

在第二次调用 start() 方法的时候,线程可能处于终止或者其他(非 NEW)状态,但是不论如何,都是不可以再次启动的。

考点分析

今天的问题可以算是个常见的面试热身题目,前面的给出的典型回答,算是对基本状态和简单流转的一个介绍,如果觉得还不够直观,我在下面分析会对比一个状态图进行介绍。总的来说,理解线程对于我们日常开发或者诊断分析,都是不可或缺的基础。

面试官可能会以此为契机,从各种不同角度考察你对线程的掌握:

相对理论一些的面试官可以会问你线程到底是什么以及 Java 底层实现方式。

线程状态的切换,以及和锁等并发工具类的互动。

线程编程时容易踩的坑与建议等。

可以看出,仅仅是一个线程,就有非常多的内容需要掌握。我们选择重点内容,开始进入详细分析。

知识扩展

首先,我们来整体看一下线程是什么?

从操作系统的角度,可以简单认为,线程是系统调度的最小单元,一个进程可以包含多个线程,作为任务的真正运作者,有自己的栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等。

在具体实现中,线程还分为内核线程、用户线程,Java 的线程实现其实是与虚拟机相关的。对于我们最熟悉的 Sun/Oracle JDK,其线程也经历了一个演进过程,基本上在 Java 1.2 之后,JDK 已经抛弃了所谓的Green Thread,也就是用户调度的线程,现在的模型是一对一映射到操作系统内核线程。

如果我们来看 Thread 的源码,你会发现其基本操作逻辑大都是以 JNI 形式调用的本地代码。

private native void start0();
private native void setPriority0(int newPriority);
private native void interrupt0();

这种实现有利有弊,总体上来说,Java 语言得益于精细粒度的线程和相关的并发操作,其构建高扩展性的大型应用的能力已经毋庸置疑。但是,其复杂性也提高了并发编程的门槛,近几年的 Go 语言等提供了协程(coroutine),大大提高了构建并发应用的效率。于此同时,Java 也在Loom项目中,孕育新的类似轻量级用户线程(Fiber)等机制,也许在不久的将来就可以在新版 JDK 中使用到它。

下面,我来分析下线程的基本操作。如何创建线程想必你已经非常熟悉了,请看下面的例子:

Runnable task = () -> {System.out.println("Hello World!");};
Thread myThread = new Thread(task);
myThread.start();
myThread.join();

我们可以直接扩展 Thread 类,然后实例化。但在本例中,我选取了另外一种方式,就是实现一个 Runnable,将代码逻放在 Runnable 中,然后构建 Thread 并启动(start),等待结束(join)。

Runnable 的好处是,不会受 Java 不支持类多继承的限制,重用代码实现,当我们需要重复执行相应逻辑时优点明显。而且,也能更好的与现代 Java 并发库中的 Executor 之类框架结合使用,比如将上面 start 和 join 的逻辑完全写成下面的结构:

Future future = Executors.newFixedThreadPool(1)
.submit(task)
.get();

这样我们就不用操心线程的创建和管理,也能利用 Future 等机制更好地处理执行结果。线程生命周期通常和业务之间没有本质联系,混淆实现需求和业务需求,就会降低开发的效率。

从线程生命周期的状态开始展开,那么在 Java 编程中,有哪些因素可能影响线程的状态呢?主要有:

线程自身的方法,除了 start,还有多个 join 方法,等待线程结束;yield 是告诉调度器,主动让出 CPU;另外,就是一些已经被标记为过时的 resume、stop、suspend 之类,据我所知,在 JDK 最新版本中,destory/stop 方法将被直接移除。

基类 Object 提供了一些基础的 wait/notify/notifyAll 方法。如果我们持有某个对象的 Monitor 锁,调用 wait 会让当前线程处于等待状态,直到其他线程 notify 或者 notifyAll。所以,本质上是提供了 Monitor 的获取和释放的能力,是基本的线程间通信方式。

并发类库中的工具,比如 CountDownLatch.await() 会让当前线程进入等待状态,直到 latch 被基数为 0,这可以看作是线程间通信的 Signal。

我这里画了一个状态和方法之间的对应图:

在这里插入图片描述

Thread 和 Object 的方法,听起来简单,但是实际应用中被证明非常晦涩、易错,这也是为什么 Java 后来又引入了并发包。总的来说,有了并发包,大多数情况下,我们已经不再需要去调用 wait/notify 之类的方法了。

前面谈了不少理论,下面谈谈线程 API 使用,我会侧重于平时工作学习中,容易被忽略的一些方面。

先来看看守护线程(Daemon Thread),有的时候应用中需要一个长期驻留的服务程序,但是不希望其影响应用退出,就可以将其设置为守护线程,如果 JVM 发现只有守护线程存在时,将结束进程,具体可以参考下面代码段。注意,必须在线程启动之前设置。


Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
daemonThread.start();

再来看看Spurious wakeup。尤其是在多核 CPU 的系统中,线程等待存在一种可能,就是在没有任何线程广播或者发出信号的情况下,线程就被唤醒,如果处理不当就可能出现诡异的并发问题,所以我们在等待条件过程中,建议采用下面模式来书写。


// 推荐
while ( isCondition()) {
waitForAConfition(...);
}

// 不推荐,可能引入bug
if ( isCondition()) {
waitForAConfition(...);
}

Thread.onSpinWait(),这是 Java 9 中引入的特性。我在专栏第 16 讲给你留的思考题中,提到“自旋锁”(spin-wait, busy-waiting),也可以认为其不算是一种锁,而是一种针对短期等待的性能优化技术。“onSpinWait()”没有任何行为上的保证,而是对 JVM 的一个暗示,JVM 可能会利用 CPU 的 pause 指令进一步提高性能,性能特别敏感的应用可以关注。

再有就是慎用ThreadLocal,这是 Java 提供的一种保存线程私有信息的机制,因为其在整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模块之间传递信息,比如事务 ID、Cookie 等上下文相关信息。

它的实现结构,可以参考源码,数据存储于线程相关的 ThreadLocalMap,其内部条目是弱引用,如下面片段。


static class ThreadLocalMap {
  static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;
      Entry(ThreadLocal<?> k, Object v) {
          super(k);
      value = v;
      }
      }
   // …
}

当 Key 为 null 时,该条目就变成“废弃条目”,相关“value”的回收,往往依赖于几个关键点,即 set、remove、rehash。

下面是 set 的示例,我进行了精简和注释:


private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  int i = key.threadLocalHashCode & (len-1);

  for (Entry e = tab[i];;) {
      //…
      if (k == null) {
// 替换废弃条目
          replaceStaleEntry(key, value, i);
          return;
      }
       }

  tab[i] = new Entry(key, value);
  int sz = ++size;
//  扫描并清理发现的废弃条目,并检查容量是否超限
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();// 清理废弃条目,如果仍然超限,则扩容(加倍)
}  

具体的清理逻辑是实现在 cleanSomeSlots 和 expungeStaleEntry 之中,如果你有兴趣可以自行阅读。

结合专栏第 4 讲介绍的引用类型,我们会发现一个特别的地方,通常弱引用都会和引用队列配合清理机制使用,但是 ThreadLocal 是个例外,它并没有这么做。

这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束,进而回收相应 ThreadLocalMap!这就是很多 OOM 的来源,所以通常都会建议,应用一定要自己负责 remove,并且不要和线程池配合,因为 worker 线程往往是不会退出的。

今天,我介绍了线程基础,分析了生命周期中的状态和各种方法之间的对应关系,这也有助于我们更好地理解 synchronized 和锁的影响,并介绍了一些需要注意的操作,希望对你有所帮助。

一课一练

关于今天我们讨论的题目你做到心中有数了吗?今天我准备了一个有意思的问题,写一个最简单的打印 HelloWorld 的程序,说说看,运行这个应用,Java 至少会创建几个线程呢?然后思考一下,如何明确验证你的结论,真实情况很可能令你大跌眼镜哦。

请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。

应关系,这也有助于我们更好地理解 synchronized 和锁的影响,并介绍了一些需要注意的操作,希望对你有所帮助。

一课一练

关于今天我们讨论的题目你做到心中有数了吗?今天我准备了一个有意思的问题,写一个最简单的打印 HelloWorld 的程序,说说看,运行这个应用,Java 至少会创建几个线程呢?然后思考一下,如何明确验证你的结论,真实情况很可能令你大跌眼镜哦。

请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。

你的朋友是不是也在准备面试呢?你可以“请朋友读”,把今天的题目分享给好友,或许你能帮到他。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/377372.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何编程实现从多数据库操作数据

对于数据量很大的复杂系统&#xff0c;有时候会采用分库或者分表的减轻单台数据库服务器压力&#xff0c;截止目前有一些工具直接支持读写分离等&#xff0c;例如ShardingSphere&#xff0c;如果不采用工具框架&#xff0c;从编码出发&#xff0c;如何实现从多个数据库读写数据…

FFmpeg 中的多线程解码

1.共享变量的互斥互斥锁&#xff08;mutex-lock&#xff09;是一种信号量&#xff0c;用来防止两个线程在同一时刻访问相同的共享资源&#xff0c;它有锁定状态和非锁定状态。在任意时刻&#xff0c;一个线程要想存取共享数据&#xff0c;线程必须首先获得mutex-lock&#xff0…

一种全新的图像变换理论的实验(四)——研究目的替代DCT和小波

一、前言 2023年02月28日凌晨1点 以前我定义为这个算法是滤波算法&#xff0c;实则上应该算是一种新变换算法&#xff0c;比如傅里叶变换&#xff08;FFT&#xff09;、离散余弦变换&#xff08;DCT&#xff09;&#xff0c;以及小波变换。所以就把所有的标题改变了一下。 本次…

MySQL的InnoDB 三种行锁,SQL 语句加了哪些锁?

InnoDB 三种行锁&#xff1a; Record Lock&#xff08;记录锁&#xff09;&#xff1a;锁住某一行记录 Gap Lock&#xff08;间隙锁&#xff09;&#xff1a;锁住一段左开右开的区间 Next-key Lock&#xff08;临键锁&#xff09;&#xff1a;锁住一段左开右闭的区间 哪些语句…

前端面试题 —— HTML

目录 一、src 和 href 的区别 二、对 HTML 语义化的理解 三、DOCTYPE(⽂档类型) 的作⽤ 四、script 标签中 defer 和 async 的区别 五、常⽤的 meta 标签有哪些&#xff1f; 六、HTML5 有哪些更新 八、行内元素有哪些&#xff1f;块级元素有哪些&#xff1f; 空(void)元素…

【正点原子FPGA连载】第十九章FreeRtos Hello World实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南

1&#xff09;实验平台&#xff1a;正点原子MPSoC开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id692450874670 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第十九章FreeRto…

项目调研丨以太坊再质押项目EigenLayer白皮书四大看点(内附完整版中文白皮书)

北京时间2月21日下午&#xff0c;被众多一线投研机构视为2023年以太坊最重要的创新&#xff0c;有可能开启以太坊新叙事方向的项目Eigenlayer终于披露了其第一版白皮书。EigenLayer是以太坊的再质押集&#xff0c;允许共识层ETH质押者选择验证构建在以太坊生态系统之上的新软件…

第七节 面向对象

面向对象 1.类和对象是什么? )类:是共同特征的描述(设计图);对象:是真实存在的具体实例。 2.如何设计类? public class 类名 { 1、成员变量(代表属性的,一般是名词) 2、成员方法(代表行为的&#xff0c;一般是动词) 3.如何创建对象? 类名对象名new 类名(); 4.拿到对象后怎么…

ubuntu 编译安装支持CUDA的OpenCV

安装须知 cuda支持 在安装完“ linux CUDAtoolkitcudnntensorrt 的安装”之后进行支持cuda的opencv安装 否则报错&#xff1a;CMake Error at modules/dnn/CMakeLists.txt:41 (message): DNN: CUDA backend requires CUDA Toolkit. Please resolve dependency or disable OPE…

Ubuntu 安装指定版本 Mysql,并设置远程连接(以安装mysql 5.5 为例)

目录 一、安装Mysql 1、卸载Mysql&#xff08;可跳过&#xff09; 2、安装mysql 软件源 3、安装mysql 5.5 4、验证测试 二、设置远程登录 1、允许使用root账号远程连接 2、Mysql 允许远程登录 一、安装Mysql 1、卸载Mysql&#xff08;可跳过&#xff09; 如果之前安装…

数据结构六大排序

1.插入排序 1.插入排序 思路&#xff1a; 从第一个元素开始认为是有序的&#xff0c;去一个元素tem从有序序列从后往前扫描&#xff0c;如果该元素大于tem&#xff0c;将该元素一刀下一位&#xff0c;循环步骤3知道找到有序序列中小于等于的元素将tem插入到该元素后&#xff0…

卡特兰数

文章目录1、简介1.1 何为卡特兰数1.2 卡特兰数的通项公式2、应用2.1 题目1&#xff1a;括号合法题目描述思路分析2.2 题目2&#xff1a;进出栈的方式2.2.1 题目描述2.2.2 思路分析2.3 题目3&#xff1a;合法的序列2.3.1 题目描述2.3.2 思路分析2.3.3 代码实现2.4 题目4&#xf…

分布式ID生成系统

目录背景常用分布式ID生成系统介绍UUIDSnowflake背景 在大多数复杂的分布式系统中&#xff0c;往往需要对大量的数据和消息进行唯一标识。而对分布式系统后台数据库的分库分表后需要有一个唯一的ID来表示一条数据或者是消息。那么我们分布式系统ID一般都有哪些需求呢&#xff1…

IP地址、主机名、域名解析(DNS)

1.什么是IP地址 每一台联网的电脑都会有一个地址&#xff0c;用于和其他计算机进行通讯 IP地址主要有两个版本&#xff1a;v4 v6 IPV4版本的地址格式名为&#xff1a;a.b.c.d&#xff0c;其中abcd表示0-225的数字&#xff0c;如192.168.88.10为一个标准地址 查看IP地址&#x…

Android 蓝牙开发——HCI log 分析(二十)

HCI log 是用来分析蓝牙设备之间的交互行为是否符合预期,是否符合蓝牙规范。对于蓝牙开发者来说,通过 HCI log 可以帮助我们更好地分析问题,理解蓝牙协议。 一、抓取HCI log 1、手机抓取HCI log 在开发者选项中打开启用蓝牙HCI信息收集日志开关,Android系统就开始自动地收…

计算机SCI论文选题和投稿需要注意什么? - 易智编译EaseEditing

科研创新与选题 科研创新是至关重要的&#xff0c;往往关系到论文是否顺利发表。 摆在我们面前的&#xff0c;往往是别人挑剩下的资料&#xff0c;似乎毫无写作价值&#xff0c;很多人便知难而退&#xff0c;干脆不写论文了。 其实&#xff0c;应该问问自已“我有什么&#xf…

Flask应用的基本组成部分、模板引擎Jinja2的使用、Flask-WTF、SQLAlchemy

目录标题1. Flask应用的基本组成部分1.1 路由&#xff08;Routing&#xff09;1.2 视图函数&#xff08;View Function&#xff09;1.3 请求&#xff08;Request&#xff09;1.4 响应&#xff08;Response&#xff09;2. 模板引擎Jinja2的使用2.1 入门案例2.2 条件判断2.3 循环…

Python采集m3u8格式做个小姐姐动态壁纸~

人生苦短&#xff0c;我用python 首先&#xff0c;我和大家一样喜欢看小姐姐~ 其次&#xff0c;看美丽的事物会让人更加有动力去… 我编不下去了哈哈哈&#xff0c;我就是爱看充满美感的人儿~ 更多python好看的:点击此处跳转文末名片获取 环境 Pythonpycharm 模块使用 第…

使用virtualenv和pip构建项目所需的独立Python环境

1、为什么需要独立的Python环境&#xff1f;在讲技术前&#xff0c;想先讲讲目的。为什么我们需要独立的Python环境&#xff1f;这里就借用virtualenv的文档来解释吧。virtualenv is a tool to create isolated Python environments.The basic problem being addressed is one …

51-Jenkins-Periodic Backup插件实现Jenkins备份

Periodic Backup插件实现Jenkins备份前言目录结构插件备份安装插件使用插件前言 本篇来学习下使用Periodic Backup插件实现Jenkins备份 目录结构 Jenkins的所有数据都是存放在文件中的&#xff0c;所以&#xff0c;Jenins备份其实就是备份Jenkins_HOME目录。 Jenkins_Home目…