在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。下面代码摘自java.lang.Thread中对线程进行初始化的部分。
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();
// 将daemon、priority属性设置为父线程的对应属性
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.target = target;
setPriority(priority);
// 将父线程的InheritableThreadLocal复制过来
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* 分配一个线程id */
tid = nextThreadID();
在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child 线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的 ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。
打开Thread类的源码。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
可以看到初始的线程命名就是Thread-threadInitNumber
。就是Thread加一个自增数字,例如:Thread-1
查看Thread类的构造函数发现。有几个提供名字的构造函数。
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name) {
init(group, target, name, 0);
}
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
init(group, target, name, stackSize);
}
不论你使用的是默认的函数命名规则,还是指定了一个特殊的名字,在线程启动之前还有一个机会可以对其进行修改,一旦线程启动,名字将不再被修改。
public final synchronized void setName(String name) {
checkAccess();
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
//线程不是NEW状态不行
if (threadStatus != 0) {
setNativeName(name);
}
}
线程对象在初始化完成之后,调用start()方法就可以启动这个线程。
线程start()方法的 含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即 启动调用start()方法的线程。
注意 启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。
public synchronized void start() {
//表示刚新建的状态为NEW的线程,threadStatus=0 如果启动多次就会抛异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
//线程启动之后会加入到一个线程组
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
线程启动的run方法其实是被start0()
启动的。start0()
是一个JNI方法。
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进 行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的 interrupt()方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是 否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。 如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted() 时依旧会返回false。
从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如 Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先 将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法 将会返回false。
在上面提到的中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个 boolean变量来控制是否需要停止任务并终止该线程。
在下面的代码中,创建了一个线程CountThread,它不断地进行变量累加,而主线程尝试对其进行中断操作和停止操作。
package com.example.demo.thread;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.TimeUnit;
/**
* @author : pengweiwei
* @date : 2020/2/1 2:57 下午
*/
public class Shutdown {
public static void main(String[] args) throws Exception {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
示例在执行过程中,main线程通过中断操作和cancel()方法均可使CountThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。