Java中一个线程可以多次start吗?
答:不可以。
原因:new Thread().start()通过Thread实例的start(),一个Thread实例只能产生一个线程。Thread实例对象调用完start方法,其状态从New初始态变成Runable等待运行(对应操作系统线程的Ready、Running两个状态)。
哈哈,上面问题回答的是不是一点毛病都没有,面试官也给你来个大拇指。
那哥们儿不就成标题党了!!!!
啥都不说了,上代码:
上结果:是不是没毛病,前面第二次调用start确实报错失败,但是后面死循环调start咋地就那么成功!!!为啥后面死循环就能重复多次调用start??
线程打印ABC代码
class PrintThread extends Thread{
private Thread t;
private String str;
private int count;
public PrintThread(Thread tmp,String str){
this.t=tmp;
this.str=str;
}
public void setCount(int count){
this.count=count;
}
@SneakyThrows
@Override
public void run() {
if(this.t!=null) {
this.t.join();
}
ZhouxxTool.printNoTimeAndThread("第"+this.count+"次打印:【"+this.str+"】");
ZhouxxTool.sleep(10);
}
}
Java中一个线程多次start报错的原因
一个线程对象只能调用一次start方法.从new到等待运行是单行道,所以如果你对一个已经启动的线程对象再调用一次start方法的话,会产生:IllegalThreadStateException异常。
start()方法源码:
public synchronized void start() {
// 如果线程不是"NEW状态",则抛出异常!
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 将线程添加到ThreadGroup中
group.add(this);
boolean started = false;
try {
// 通过start0()启动线程,新线程会调用run()方法
start0();
// 设置started标记=true
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// start0是native方法,也就是说start0是直接调用了操作系统提供的线程开启方法。
private native void start0();
- 通过断点跟踪,可以看到当线程对象第一次调用start方法时会进入同步方法,会判断threadStatus是否为0,如果为0,则进行往下走,否则抛出非法状态异常;
- 将当前线程对象加入线程组;
- 调用本地方法start0执行真正的创建线程工作,并调用run方法,可以看到在start0执行完后,threadStatus的值发生了改变,不再为0;
- finally块用于捕捉start0方法调用发生的异常。
线程是如何根据threadStatus
来判断线程的状态的呢?
通过查看Thread提供的public方法getState可以看到,调用的是sun.misc.VM.toThreadState(threadStatus),根据位运算得出线程的不同状态:
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
当start调用后,并且run方法内容执行完后,线程是如何终止的呢?实际上是由虚拟机调用Thread中的exit方法来进行资源清理并终止线程的,看下exit方法源码:
/**
* 系统调用该方法用于在线程实际退出之前释放资源
*/
private void exit() {
//释放线程组资源
if (group != null) {
group.threadTerminated(this);
group = null;
}
//清理run方法实例对象
target = null;
/*加速资源释放。快速垃圾回收 */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
- 到这里,t1 线程经历了从新建(NEW),就绪(RUNNABLE),运行(RUNNING),定时等待(TIMED_WAITING),终止(TERMINATED)这样一个过程;
- 由于在第一次 start 方法后,threadStatus 值被改变,因此第二次调用start时会抛出非法状态异常;
- 在调用start0方法后,如果run方法体内容被快速执行完,那么系统会自动调用exit方法释放资源,销毁对象,所以第二次调用start方法时,有可能内部资源已经被释放。
至此,可以证明为啥start只能被调用一次了。
是不是很尴尬,O(∩_∩)O
其实说了这么多,就是让大家清楚为啥上面代码中“死循环能够多次调用start”。
接下来,继续整理一下线程的生命周期及其状态机。
Java中线程的生命周期
Java中线程状态
下图诠释了线程状态流转
线程从创建到死亡是会经历多个状态的流转的。它们分别是:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
。
// Thread类的内部的枚举类定义了线程的状态
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
为什么线程启动是调用start方法来启动线程而不能直接调用run方法?
从上图中,我们就可以看到,在New了一个线程后,首先进入初始态,然后调用start()方法来到就绪态,这里并不会立即执行线程里的任务,而是会等待系统资源分配,当分配到时间片后就可以开始运行了。
start()方法是一个native方法,它将启动一个新线程,并执行run()方法,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行(单线程操作),并不会在某个线程中执行它(多线程操作),所以这并不是多线程工作。
这就是为什么调用 start() 方法时会执行 run() 方法,为什么不能直接调用 run() 方法的原因了。
为什么前面截图中,死循环可以多次重复执行start?
还是回到最开始的地方:start的源码
/**线程成员变量,默认为0,volatile修饰可以保证线程间可见性*/
private volatile int threadStatus = 0;
/* 当前线程所属的线程组 */
private ThreadGroup group;
/**
* 同步方法,同一时间,只能有一个线程可以调用此方法
*/
public synchronized void start() {
//threadStatus
if (threadStatus != 0)
throw new IllegalThreadStateException();
//线程组
group.add(this);
boolean started = false; // 第二次执行start0会抛异常,这时started仍然为false
try {
//本地方法,该方法会实际调用run方法
start0();
started = true;
} finally {
try {
if (!started) {
//创建失败,则从线程组中删除该线程
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* start0抛出的异常不用处理,将会在堆栈中传递 */
}
}
}
那么为了让线程能多次执行start,就需要完成下面操作:
- 在第一次调用start方法后(调用一下join,确保当前start是执行完成的),然后修改threadStatus值,这样在第二次调用时可以跳过状态值判断,达到多次调用start方法的目的;
- 当我第二次调用t1.start时,防止系统调用exit方法清理线程资源;比如:将group = null;这个时候start也会报错空指针。
- 经过以上两步,我成功绕开 threadStatus 判断和线程组增加方法,开始执行start0方法,但是在执行start0的时候抛出异常,并走到了finally块中,由于start为false,所以会执行group.threadStartFailed(this)操作,将该线程从线程组中移除;
- 至此基本可以完成start重复执行的操作。
继续之前的案例:三个线程交替打印A、B、C
原因很简单:修改线程状态,将线程状态改成0,new初始态,再调用start,就不会报错了,大家可以自行尝试一下。
详细代码:创建三个线程,每个线程单独打印一个字符串,三个线程轮询打印A,B,C
package com.zxx.study.base.thread;
import lombok.SneakyThrows;
/**
* @author zhouxx
* @create 2024-08-10 14:46
*/
public class ThreadJoinTest3 {
@SneakyThrows
public static void main(String[] args) {
PrintThread printThreadA=new PrintThread(null,"A");
PrintThread printThreadB=new PrintThread(printThreadA,"B");
PrintThread printThreadC=new PrintThread(printThreadB,"C");
try {
printThreadA.start();
printThreadB.start();
printThreadC.start();
printThreadA.join();
printThreadB.join();
printThreadC.join();
// todo 第二次调用用 start
printThreadA.start();
printThreadB.start();
printThreadC.start();
printThreadA.join();
printThreadB.join();
printThreadC.join();
}catch (Exception e){
e.printStackTrace();
}
// todo 死循环调用 Thread.start
int count=1;
while (true) {
printThreadA.setCount(count);
printThreadB.setCount(count);
printThreadC.setCount(count);
ThreadJoinTest1.modifyThreadStatus(printThreadA, printThreadB, printThreadC);
// todo 第N次调用用 start
printThreadA.start();
printThreadB.start();
printThreadC.start();
printThreadA.join();
printThreadB.join();
printThreadC.join();
count++;
}
}
}