Java 线程生命周期的不同状态
在Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中。
- 新建(NEW),表示线程被创建处理还没有真正启动的状态,可以认为它是一个Java 内部状态。
- 就绪(RUNNABLE),表示该线程已经在JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它CPU 片段,在就绪队列里面排队。
- 阻塞(BLOCKED),表示线程在等待 Monitor lock。比如,线程试图通过synchronized 去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。
- 等待(WAITING),表示正在等待其他线程采取某些操作。一个场景的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似notify 等动作,通知消费线程可以继续工作了。Thread.join() 也会让线程进入等待状态。
- 计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如wait 或 join 等方法的指定超时版本,如下所示:
public final native void wait(long timeout) throws InterruptedException;
- 终止(TERMINATED),不管是意外退出,还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。
join() 的作用
让父线程等待子线程结束之后才能继续运行。
当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。
真正发生的是调用 join() 的线程进入 TIMED_WAITING 状态,等待 join() 所属线程运行结束后再继续运行。
// 父线程
public class Parent extends Thread {
public void run() {
Child child = new Child();
child.start();
child.join();
// ...
}
}
// 子线程
public class Child extends Thread {
public void run() {
// ...
}
}
上面代码展示了两个类:Parent(父线程类),Child(子线程类)。
- 在 Parent.run() 中,通过 Child child = new Child(); 新建 child 子线程(此时 child 处于 NEW 状态);
- 然后再调用 child.start()(child 转换为 RUNNABLE 状态);
- 再调用 child.join()。
在 Parent 调用 child.join() 后,child 子线程正常运行,Parent 父线程会等待 child 子线程结束后再继续运行。
- 一个进程可以保护多个线程,作为任务的真正运行者,有自己的栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等。
- 在Java 1.2 之后,Java 线程模型是一对一映射到操作系统内核线程的。
示例代码:
public class HelloWorldThread {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Runnable task = ()->{System.out.println("Hello World!");};
Thread myThread = new Thread(task);
myThread.start();
myThread.join();
System.out.println("done");
}
}
查看Java应用中基本线程
写一个最简单的打印 HelloWorld 的程序,获取当前Java 所有线程:
public class HowManyThreads {
public static void main(String[] args) {
System.out.println("Hello world.");
ThreadGroup group = Thread.currentThread().getThreadGroup();
ThreadGroup topGroup = group;
//遍历线程组树,获取根线程组
while(group != null){
topGroup = group;
group = group.getParent();
}
int nowThreads = topGroup.activeCount();
Thread[] lstThreads = new Thread[nowThreads];
// 获取根线程组的所有线程
topGroup.enumerate(lstThreads);
for(int i=0; i
System.out.println("线程 number: " + i + " = " + lstThreads[i].getName());
}
}
}
输出结果(IDEA IDE 中运行结果):
Hello world.
线程 number: 0 = Reference Handler
线程 number: 1 = Finalizer
线程 number: 2 = Signal Dispatcher
线程 number: 3 = Attach Listener
线程 number: 4 = main
线程 number: 5 = Monitor Ctrl-Break
在命令行模式下,运行结果:
解释如下:
- Reference Handler :它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。
- Finalizer: 用来执行所有用户Finalizer 方法的线程。
- Attach Listener :线程是负责接收到外部的命令。
对该命令进行执行,并且把结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反馈信息,如:java -version、jmap、jstack等等。如果该线程在jvm启动的时候没有初始化,那么,则会在用户第一次执行jvm命令时,得到启动。
- Signal Dispatcher: 外部jvm命令的转发器;
前面我们提到第一个Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispatcher线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispatcher线程也是在第一次接收外部jvm命令时,进行初始化工作。
- Monitor Ctrl-Break : 如果使用的IDE是IDEA 直接运行会多一个Monitor Ctrl-break线程,这个是IDE的原因。
- main : Java 应用主线程;