一个线程两次调用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)状态,但是不论如何,都是不可以再次启动的。

知识扩展

1、Java中的线程

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

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

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

2、使用Runnable

虽然可以通过直接扩展Thread类并实例化它的方式创建线程,但是我们建议使用Runnable。

扫描二维码关注公众号,回复: 2522045 查看本文章
Runnable task = () -> {System.out.println("Hello World!");};
Thread myThread = new Thread(task);
myThread.start();
myThread.join();

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

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

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

3、影响线程状态的因素

这里列出了导致线程状态发生改变的操作:

  • 来自线程自身的方法,除了start,还有多个join方法,等待线程结束;yield是告诉调度器,主动让出CPU;另外,就是一些已经被标记为过时的resume、stop、suspend之类。从Java 9开始,destroy/stop方法将被直接移除。
  • 基类Object提供了一些基础的wait/notify/notifyAll方法。如果我们持有某个对象的Monitor锁,调用wait会让当前线程处于等待状态,直到其它线程notify或者notifyAll。所以,本质上是提供了Monitor的获取和释放的能力,是基本的线程间通讯方式。
  • 并发类库中的工具,比如CountDownLatch.await()会让当前线程进入等待状态,直到latch被计数为0,这可以看作是线程间通信的Signal。

以下是线程状态变化图:

4、慎用ThreadLocal

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。

通常弱引用都会和引用队列配合清理机制使用,但是ThreadLocal是个例外,它并没有这么做。这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束,进而回收相应ThreadLocalMap!这就是很多OOM的来源。所以通常都会建议,应用一定要自己负责remove,并且不要和线程池配合,因为worker线程往往是不会退出的。

【完】

猜你喜欢

转载自blog.csdn.net/qweqwruio/article/details/81359721