java

java并发-线程的启动和终止

4.2线程的启动和终止

4.2.1 构造线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要 的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。代码清单4-6所示的 代码摘自java.lang.Thread中对线程进行初始化的部分。

private void init(ThreadGroup g, Runnable target, String name,long stackSize,AccessControlContext acc) {
	if (name == null) {
		throw new NullPointerException("name cannot be null");
	}
	// 当前线程就是该线程的父线程
	Thread parent = currentThread();
	this.group = g;
	// 将daemon、priority属性设置为父线程的对应属性
	this.daemon = parent.isDaemon();
	this.priority = parent.getPriority();
	this.name = name.toCharArray();
	this.target = target;
	setPriority(priority);
// 将父线程的InheritableThreadLocal复制过来
	if (parent.inheritableThreadLocals != null)
		this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
	// 分配一个线程ID
	tid = nextThreadID();
}

在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程 继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的 ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对 象就初始化好了,在堆内存中等待着运行。

4.2.2 启动线程

线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义 是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用 start()方法的线程。

注意 启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程 序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。

4.2.3 理解中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行 了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt() 方法对其进行中断操作。

Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。 java中断模型也是这么简单,每个线程对象里都有一个boolean类型的标识(不一定就要是Thread类的字段,实际上也的确不是,这几个方法最终都是通过native方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。

通常,中断的使用场景有以下几个:

  • 点击某个桌面应用中的取消按钮时;
  • 某个操作超过了一定的执行时间限制需要中止时;
  • 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
  • 一组线程中的一个或多个出现错误导致整组都无法继续时;
  • 当一个应用或服务需要停止时。

4.2.4 过期的suspend()、resume()和stop()

suspend()、resume()和stop()方法完成了线程的暂停、恢复和终 止工作,而且非常“人性化”。但是这些API是过期的,也就是不建议使用的。

不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结 一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会, 因此会导致程序可能工作在不确定状态下。

注意 正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建 议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。

4.2.5 安全地终止线程

在4.2.3节中提到的中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互 方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变 量来控制是否需要停止任务并终止该线程。

在代码清单4-9所示的例子中,创建了一个线程CountThread,它不断地进行变量累加,而 主线程尝试对其进行中断操作和停止操作。

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得以终止。 这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地 将线程停止,因此这种终止线程的做法显得更加安全和优雅。

关于作者

程序员,软件工程师,java, golang, rust, c, python,vue, Springboot, mybatis, mysql,elasticsearch, docker, maven, gcc, linux, ubuntu, centos, axum,llm, paddlepaddle, onlyoffice,minio,银河麒麟,中科方德,rpm